ASP.NET Core 中的 Razor 頁(yè)面和 EF Core - 并發(fā)

2019-04-17 08:58 更新

本教程介紹如何處理多個(gè)用戶并發(fā)更新同一實(shí)體(同時(shí))時(shí)出現(xiàn)的沖突。 如果遇到無(wú)法解決的問(wèn)題,請(qǐng)下載或查看已完成的應(yīng)用。 下載說(shuō)明。

并發(fā)沖突

在以下情況下,會(huì)發(fā)生并發(fā)沖突:

  • 用戶導(dǎo)航到實(shí)體的編輯頁(yè)面。
  • 第一個(gè)用戶的更改還未寫(xiě)入數(shù)據(jù)庫(kù)之前,另一用戶更新同一實(shí)體。

如果未啟用并發(fā)檢測(cè),當(dāng)發(fā)生并發(fā)更新時(shí):

  • 最后一個(gè)更新優(yōu)先。 也就是最后一個(gè)更新的值保存至數(shù)據(jù)庫(kù)。
  • 第一個(gè)并發(fā)更新將會(huì)丟失。

開(kāi)放式并發(fā)

樂(lè)觀并發(fā)允許發(fā)生并發(fā)沖突,并在并發(fā)沖突發(fā)生時(shí)作出正確反應(yīng)。 例如,Jane 訪問(wèn)院系編輯頁(yè)面,將英語(yǔ)系的預(yù)算從 350,000.00 美元更改為 0.00 美元。

將預(yù)算更改為零

在 Jane 單擊“保存”之前,John 訪問(wèn)了相同頁(yè)面,并將開(kāi)始日期字段從 2007/1/9 更改為 2013/1/9。

將開(kāi)始日期更改為 2013

Jane 先單擊“保存”,并在瀏覽器顯示索引頁(yè)時(shí)看到她的更改。

預(yù)算已更改為零

John 單擊“編輯”頁(yè)面上的“保存”,但頁(yè)面的預(yù)算仍顯示為 350,000.00 美元。 接下來(lái)的情況取決于并發(fā)沖突的處理方式。

樂(lè)觀并發(fā)包括以下選項(xiàng):

  • 可以跟蹤用戶已修改的屬性,并僅更新數(shù)據(jù)庫(kù)中相應(yīng)的列。在這種情況下,數(shù)據(jù)不會(huì)丟失。 兩個(gè)用戶更新了不同的屬性。 下次有人瀏覽英語(yǔ)系時(shí),將看到 Jane 和 John 兩個(gè)人的更改。 這種更新方法可以減少導(dǎo)致數(shù)據(jù)丟失的沖突數(shù)。 這種方法:無(wú)法避免數(shù)據(jù)丟失,如果對(duì)同一屬性進(jìn)行競(jìng)爭(zhēng)性更改的話。通常不適用于 Web 應(yīng)用。 它需要維持重要狀態(tài),以便跟蹤所有提取值和新值。 維持大量狀態(tài)可能影響應(yīng)用性能。可能會(huì)增加應(yīng)用復(fù)雜性(與實(shí)體上的并發(fā)檢測(cè)相比)。
  • 可讓 John 的更改覆蓋 Jane 的更改。下次有人瀏覽英語(yǔ)系時(shí),將看到 2013/9/1 和提取的值 350,000.00 美元。 這種方法稱為“客戶端優(yōu)先”或“最后一個(gè)優(yōu)先”方案。 (客戶端的所有值優(yōu)先于數(shù)據(jù)存儲(chǔ)的值。)如果不對(duì)并發(fā)處理進(jìn)行任何編碼,則自動(dòng)執(zhí)行“客戶端優(yōu)先”。
  • 可以阻止在數(shù)據(jù)庫(kù)中更新 John 的更改。 應(yīng)用通常會(huì):顯示錯(cuò)誤消息。顯示數(shù)據(jù)的當(dāng)前狀態(tài)。允許用戶重新應(yīng)用更改。這稱為“存儲(chǔ)優(yōu)先”方案。 (數(shù)據(jù)存儲(chǔ)值優(yōu)先于客戶端提交的值。)本教程實(shí)施“存儲(chǔ)優(yōu)先”方案。此方法可確保用戶在未收到警報(bào)時(shí)不會(huì)覆蓋任何更改。

處理并發(fā)

當(dāng)屬性配置為并發(fā)令牌時(shí):

數(shù)據(jù)庫(kù)和數(shù)據(jù)模型必須配置為支持引發(fā) DbUpdateConcurrencyException。

檢測(cè)屬性的并發(fā)沖突

可使用 ConcurrencyCheck 特性在屬性級(jí)別檢測(cè)并發(fā)沖突。 該特性可應(yīng)用于模型上的多個(gè)屬性。 有關(guān)詳細(xì)信息,請(qǐng)參閱數(shù)據(jù)注釋 - ConcurrencyCheck。

本教程中不使用 [ConcurrencyCheck] 特性。

檢測(cè)行的并發(fā)沖突

要檢測(cè)并發(fā)沖突,請(qǐng)將 rowversion 跟蹤列添加到模型。 rowversion:

  • 是 SQL Server 特定的。 其他數(shù)據(jù)庫(kù)可能無(wú)法提供類似功能。
  • 用于確定從數(shù)據(jù)庫(kù)提取實(shí)體后未更改實(shí)體。

數(shù)據(jù)庫(kù)生成 rowversion 序號(hào),該數(shù)字隨著每次行的更新遞增。 在 Update 或 Delete 命令中,Where 子句包括 rowversion 的提取值。 如果要更新的行已更改:

  • rowversion 不匹配提取值。
  • Update 或 Delete 命令不能找到行,因?yàn)?nbsp;Where 子句包含提取的 rowversion。
  • 引發(fā)一個(gè) DbUpdateConcurrencyException。

在 EF Core 中,如果未通過(guò) Update 或 Delete 命令更新行,則引發(fā)并發(fā)異常。

向 Department 實(shí)體添加跟蹤屬性

在 Models/Department.cs 中,添加名為 RowVersion 的跟蹤屬性:

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Timestamp 特性指定此列包含在 Update 和 Delete 命令的 Where 子句中。 該特性稱為 Timestamp,因?yàn)橹鞍姹镜?SQL Server 在 SQL rowversion 類型將其替換之前使用 SQL timestamp 數(shù)據(jù)類型。

Fluent API 還可指定跟蹤屬性:

C#

modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();

以下代碼顯示更新 Department 名稱時(shí)由 EF Core 生成的部分 T-SQL:

SQL

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

前面突出顯示的代碼顯示包含 RowVersion 的 WHERE 子句。 如果數(shù)據(jù)庫(kù) RowVersion 不等于 RowVersion 參數(shù)(@p2),則不更新行。

以下突出顯示的代碼顯示驗(yàn)證更新哪一行的 T-SQL:

SQL

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

@@ROWCOUNT 返回受上一語(yǔ)句影響的行數(shù)。 在沒(méi)有行更新的情況下,EF Core 引發(fā) DbUpdateConcurrencyException。

在 Visual Studio 的輸出窗口中可看見(jiàn) EF Core 生成的 T-SQL。

更新數(shù)據(jù)庫(kù)

添加 RowVersion 屬性可更改數(shù)據(jù)庫(kù)模型,這需要遷移。

生成項(xiàng)目。 在命令窗口中輸入以下命令:

console

dotnet ef migrations add RowVersion
dotnet ef database update

前面的命令:

  • 添加 Migrations/{time stamp}_RowVersion.cs 遷移文件。
  • 更新 Migrations/SchoolContextModelSnapshot.cs 文件。 此次更新將以下突出顯示的代碼添加到 BuildModel 方法:C#復(fù)制modelBuilder.Entity("ContosoUniversity.Models.Department", b => { b.Property<int>("DepartmentID") .ValueGeneratedOnAdd(); b.Property<decimal>("Budget") .HasColumnType("money"); b.Property<int?>("InstructorID"); b.Property<string>("Name") .HasMaxLength(50); b.Property<byte[]>("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate(); b.Property<DateTime>("StartDate"); b.HasKey("DepartmentID"); b.HasIndex("InstructorID"); b.ToTable("Department"); });
  • 運(yùn)行遷移以更新數(shù)據(jù)庫(kù)。

構(gòu)架院系模型

按照為“學(xué)生”模型搭建基架中的說(shuō)明操作,并對(duì)模型類使用 Department。

上述命令為 Department 模型創(chuàng)建基架。 在 Visual Studio 中打開(kāi)項(xiàng)目。

生成項(xiàng)目。

更新院系索引頁(yè)

基架引擎為索引頁(yè)創(chuàng)建 RowVersion 列,但不應(yīng)顯示該字段。 本教程中顯示 RowVersion 的最后一個(gè)字節(jié),以幫助理解并發(fā)。 不能保證最后一個(gè)字節(jié)是唯一的。 實(shí)際應(yīng)用不會(huì)顯示 RowVersion 或 RowVersion 的最后一個(gè)字節(jié)。

更新索引頁(yè):

  • 用院系替換索引。
  • 將包含 RowVersion 的標(biāo)記替換為 RowVersion 的最后一個(gè)字節(jié)。
  • 將 FirstMidName 替換為 FullName。

以下標(biāo)記顯示更新后的頁(yè)面:

HTML

@page
@model ContosoUniversity.Pages.Departments.IndexModel

@{
    ViewData["Title"] = "Departments";
}

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Department) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Budget)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.StartDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Administrator.FullName)
            </td>
            <td>
                @item.RowVersion[7]
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

更新編輯頁(yè)模型

使用以下代碼更新 pages\departments\edit.cshtml.cs:

C#

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public EditModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                return NotFound();
            }

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            var departmentToUpdate = await _context.Departments
                .Include(i => i.Administrator)
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            // null means Department was deleted by another user.
            if (departmentToUpdate == null)
            {
                return await HandleDeletedDepartment();
            }

            // Update the RowVersion to the value when this entity was
            // fetched. If the entity has been updated after it was
            // fetched, RowVersion won't match the DB RowVersion and
            // a DbUpdateConcurrencyException is thrown.
            // A second postback will make them match, unless a new 
            // concurrency issue happens.
            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./Index");
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var exceptionEntry = ex.Entries.Single();
                    var clientValues = (Department)exceptionEntry.Entity;
                    var databaseEntry = exceptionEntry.GetDatabaseValues();
                    if (databaseEntry == null)
                    {
                        ModelState.AddModelError(string.Empty, "Unable to save. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await setDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Must clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

       private async Task<IActionResult> HandleDeletedDepartment()
        {
            Department deletedDepartment = new Department();
            // ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID); 
            return Page();
        }

        private async Task setDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. The "
              + "edit operation was canceled and the current values in the database "
              + "have been displayed. If you still want to edit this record, click "
              + "the Save button again.");
        }
    }
}

要檢測(cè)并發(fā)問(wèn)題,請(qǐng)使用來(lái)自所提取實(shí)體的 rowVersion 值更新 OriginalValue。 EF Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 命令。 如果沒(méi)有行受到 UPDATE 命令影響(沒(méi)有行具有原始 RowVersion 值),將引發(fā) DbUpdateConcurrencyException 異常。

C#

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    // null means Department was deleted by another user.
    if (departmentToUpdate == null)
    {
        return await HandleDeletedDepartment();
    }

    // Update the RowVersion to the value when this entity was
    // fetched. If the entity has been updated after it was
    // fetched, RowVersion won't match the DB RowVersion and
    // a DbUpdateConcurrencyException is thrown.
    // A second postback will make them match, unless a new 
    // concurrency issue happens.
    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

在前面的代碼中,Department.RowVersion 為實(shí)體提取后的值。 使用此方法調(diào)用 FirstOrDefaultAsync 時(shí),OriginalValue 為數(shù)據(jù)庫(kù)中的值。

以下代碼獲取客戶端值(向此方法發(fā)布的值)和數(shù)據(jù)庫(kù)值:

C#

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await setDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

以下代碼為每列添加自定義錯(cuò)誤消息,這些列中的數(shù)據(jù)庫(kù)值與發(fā)布到 OnPostAsync 的值不同:

C#

private async Task setDbErrorMessage(Department dbValues,
        Department clientValues, SchoolContext context)
{

    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    if (dbValues.StartDate != clientValues.StartDate)
    {
        ModelState.AddModelError("Department.StartDate",
            $"Current value: {dbValues.StartDate:d}");
    }
    if (dbValues.InstructorID != clientValues.InstructorID)
    {
        Instructor dbInstructor = await _context.Instructors
           .FindAsync(dbValues.InstructorID);
        ModelState.AddModelError("Department.InstructorID",
            $"Current value: {dbInstructor?.FullName}");
    }

    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit "
      + "was modified by another user after you. The "
      + "edit operation was canceled and the current values in the database "
      + "have been displayed. If you still want to edit this record, click "
      + "the Save button again.");
}

以下突出顯示的代碼將 RowVersion 值設(shè)置為從數(shù)據(jù)庫(kù)檢索的新值。 用戶下次單擊“保存”時(shí),將僅捕獲最后一次顯示編輯頁(yè)后發(fā)生的并發(fā)錯(cuò)誤。

C#

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
    var exceptionEntry = ex.Entries.Single();
    var clientValues = (Department)exceptionEntry.Entity;
    var databaseEntry = exceptionEntry.GetDatabaseValues();
    if (databaseEntry == null)
    {
        ModelState.AddModelError(string.Empty, "Unable to save. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await setDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

ModelState 具有舊的 RowVersion 值,因此需使用 ModelState.Remove 語(yǔ)句。 在 Razor 頁(yè)面中,當(dāng)兩者都存在時(shí),字段的 ModelState 值優(yōu)于模型屬性值。

更新“編輯”頁(yè)

使用以下標(biāo)記更新 Pages/Departments/Edit.cshtml:

HTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.InstructorID" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

前面的標(biāo)記:

  • 將 page 指令從 @page 更新為 @page "{id:int}"。
  • 添加隱藏的行版本。 必須添加 RowVersion,以便回發(fā)綁定值。
  • 顯示 RowVersion 的最后一個(gè)字節(jié)以進(jìn)行調(diào)試。
  • 將 ViewData 替換為強(qiáng)類型 InstructorNameSL。

使用編輯頁(yè)測(cè)試并發(fā)沖突

在英語(yǔ)系打開(kāi)編輯的兩個(gè)瀏覽器實(shí)例:

  • 運(yùn)行應(yīng)用,然后選擇“院系”。
  • 右鍵單擊英語(yǔ)系的“編輯”超鏈接,然后選擇“在新選項(xiàng)卡中打開(kāi)”。
  • 在第一個(gè)選項(xiàng)卡中,單擊英語(yǔ)系的“編輯”超鏈接。

兩個(gè)瀏覽器選項(xiàng)卡顯示相同信息。

在第一個(gè)瀏覽器選項(xiàng)卡中更改名稱,然后單擊“保存”。

更改后的“院系編輯”頁(yè) 1

瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁(yè)。 請(qǐng)注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。

在第二個(gè)瀏覽器選項(xiàng)卡中更改不同字段。

更改后的“院系編輯”頁(yè) 2

單擊“保存” 。 可看見(jiàn)所有不匹配數(shù)據(jù)庫(kù)值的字段的錯(cuò)誤消息:

“院系編輯”頁(yè)錯(cuò)誤消息

此瀏覽器窗口將不會(huì)更改名稱字段。 將當(dāng)前值(語(yǔ)言)復(fù)制并粘貼到名稱字段。 退出選項(xiàng)卡。客戶端驗(yàn)證將刪除錯(cuò)誤消息。

“院系編輯”頁(yè)錯(cuò)誤消息

再次單擊“保存”。 保存在第二個(gè)瀏覽器選項(xiàng)卡中輸入的值。 在索引頁(yè)中可以看到保存的值。

更新“刪除”頁(yè)

使用以下代碼更新“刪除”頁(yè)模型:

C#

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Departments
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

            if (Department == null)
            {
                 return NotFound();
            }

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "The delete operation was canceled and the current values in the "
                  + "database have been displayed. If you still want to delete this "
                  + "record, click the Delete button again.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

刪除頁(yè)檢測(cè)提取實(shí)體并更改時(shí)的并發(fā)沖突。 提取實(shí)體后,Department.RowVersion 為行版本。 EF Core 創(chuàng)建 SQL DELETE 命令時(shí),它包括具有 RowVersion 的 WHERE 子句。 如果 SQL DELETE 命令導(dǎo)致零行受影響:

  • SQL DELETE 命令中的 RowVersion 與數(shù)據(jù)庫(kù)中的 RowVersion 不匹配。
  • 引發(fā) DbUpdateConcurrencyException 異常。
  • 使用 concurrencyError 調(diào)用 OnGetAsync。

更新“刪除”頁(yè)

使用以下代碼更新 Pages/Departments/Delete.cshtml:

HTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<p class="text-danger">@Model.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Department.Name)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Budget)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Budget)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.StartDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.StartDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.RowVersion)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.RowVersion[7])
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department.Administrator)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Department.Administrator.FullName)
        </dd>
    </dl>
    
    <form method="post">
        <input type="hidden" asp-for="Department.DepartmentID" />
        <input type="hidden" asp-for="Department.RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>

上述標(biāo)記進(jìn)行以下更改:

  • 將 page 指令從 @page 更新為 @page "{id:int}"。
  • 添加錯(cuò)誤消息。
  • 將“管理員”字段中的 FirstMidName 替換為 FullName。
  • 更改 RowVersion 以顯示最后一個(gè)字節(jié)。
  • 添加隱藏的行版本。 必須添加 RowVersion,以便回發(fā)綁定值。

使用刪除頁(yè)測(cè)試并發(fā)沖突

創(chuàng)建測(cè)試系。

在測(cè)試系打開(kāi)刪除的兩個(gè)瀏覽器實(shí)例:

  • 運(yùn)行應(yīng)用,然后選擇“院系”。
  • 右鍵單擊測(cè)試系的“刪除”超鏈接,然后選擇“在新選項(xiàng)卡中打開(kāi)”。
  • 單擊測(cè)試系的“編輯”超鏈接。

兩個(gè)瀏覽器選項(xiàng)卡顯示相同信息。

在第一個(gè)瀏覽器選項(xiàng)卡中更改預(yù)算,然后單擊“保存”。

瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁(yè)。 請(qǐng)注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。

從第二個(gè)選項(xiàng)卡中刪除測(cè)試部門。并發(fā)錯(cuò)誤顯示來(lái)自數(shù)據(jù)庫(kù)的當(dāng)前值。 單擊“刪除”將刪除實(shí)體,除非 RowVersion 已更新,院系已刪除。

請(qǐng)參閱繼承了解如何繼承數(shù)據(jù)模型。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)