处理并发冲突

ASP.NET Core 中的 Razor 页面和 EF Core - 并发 - 第 8 个教程(共 8 个)

作者:Rick AndersonTom DykstraJon P Smith

Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。 若要了解系列教程,请参阅第一个教程

如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。

本教程介绍如何处理多个用户并发更新同一实体(同时)时出现的冲突。

并发冲突

在以下情况下,会发生并发冲突:

  • 用户导航到实体的编辑页面。
  • 第一个用户的更改还未写入数据库之前,另一用户更新同一实体。

如果未启用并发检测,则最后更新数据库的人员将覆盖其他用户的更改。 如果这种风险是可以接受的,则并发编程的成本可能会超过收益。

悲观并发(锁定)

预防并发冲突的一种方法是使用数据库锁定。 这称为悲观并发。 应用在读取要更新的数据库行之前,将请求锁定。 锁定某一行的更新访问权限之后,其他用户在第一个锁定释放之前无法锁定该行。

管理锁定有缺点。 它的编程可能很复杂,并且随着用户增加可能会导致性能问题。 Entity Framework Core 未提供对它的内置支持,并且本教程不展示其实现方式。

开放式并发

乐观并发允许发生并发冲突,并在并发冲突发生时作出正确反应。 例如,Jane 访问院系编辑页面,将英语系的预算从 350,000.00 美元更改为 0.00 美元。

将预算更改为零

在 Jane 单击“保存”之前,John 访问了相同页面,并将开始日期字段从 2007/1/9 更改为 2013/1/9 。

将开始日期更改为 2013

Jane 单击“保存”后看到更改生效,因为浏览器会显示预算金额为零的“索引”页面 。

John 单击“编辑”页面上的“保存”,但页面的预算仍显示为 350,000.00 美元 。 接下来的情况取决于并发冲突的处理方式:

  • 可以跟踪用户已修改的属性,并仅更新数据库中相应的列。

    在这种情况下,数据不会丢失。 两个用户更新了不同的属性。 下次有人浏览英语系时,将看到 Jane 和 John 两个人的更改。 这种更新方法可以减少导致数据丢失的冲突数。 这种方法具有一些缺点:

    • 无法避免数据丢失,如果对同一属性进行竞争性更改的话。
    • 通常不适用于 Web 应用。 它需要维持重要状态,以便跟踪所有提取值和新值。 维持大量状态可能影响应用性能。
    • 可能会增加应用复杂性(与实体上的并发检测相比)。
  • 可让 John 的更改覆盖 Jane 的更改。

    下次有人浏览英语系时,将看到 2013/9/1 和提取的值 350,000.00 美元。 这种方法称为“客户端优先”或“最后一个优先”方案 。 (客户端的所有值优先于数据存储的值。)如果不对并发处理进行任何编码,则自动执行“客户端优先”。

  • 可以阻止在数据库中更新 John 的更改。 应用通常会:

    • 显示错误消息。
    • 显示数据的当前状态。
    • 允许用户重新应用更改。

    这称为“存储优先”方案 。 (数据存储值优先于客户端提交的值。)本教程实施“存储优先”方案。 此方法可确保用户在未收到警报时不会覆盖任何更改。

EF Core 中的冲突检测

EF Core 在检测到冲突时会引发 DbConcurrencyException 异常。 数据模型必须配置为启用冲突检测。 启用冲突检测的选项包括以下项:

  • 配置 EF Core,在 Update 或 Delete 命令的 Where 子句中包含配置为并发令牌的列的原始值。

    调用 SaveChanges 时,Where 子句查找使用 ConcurrencyCheck 特性注释的所有属性的原始值。 如果在第一次读取行之后有任意并发令牌属性发生了更改,更新语句将无法查找到要更新的行。 EF Core 将其解释为并发冲突。 对于包含许多列的数据库表,此方法可能导致非常多的 Where 子句,并且可能需要大量的状态。 因此通常不建议使用此方法,并且它也不是本教程中使用的方法。

  • 数据库表中包含一个可用于确定某行更改时间的跟踪列。

    在 SQL Server 数据库中,跟踪列的数据类型是 rowversion rowversion 值是一个序列号,该编号随着每次行的更新递增。 在 Update 或 Delete 命令中,Where 子句包含跟踪列的原始值(原始行版本号)。 如果其他用户已更改要更新的行,则 rowversion 列中的值与原始值不同。 在这种情况下,Update 或 Delete 语句会由于 Where 子句而无法找到要更新的行。 如果 Update 或 Delete 命令未影响任何行,EF Core 会引发并发异常。

添加跟踪属性

在 Models/Department.cs 中,添加名为 RowVersion 的跟踪属性 :

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 特性用于将列标识为并发跟踪列。 Fluent API 是指定跟踪属性的另一种方法:

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

更新数据库

添加 RowVersion 属性可更改需要迁移的数据库模型。

生成项目。

此命令:

  • 创建 Migrations/{time stamp}_RowVersion.cs 迁移文件 。

  • 更新 Migrations/SchoolContextModelSnapshot.cs 文件 。 此次更新将以下突出显示的代码添加到 BuildModel 方法:

    modelBuilder.Entity("ContosoUniversity.Models.Department", b =>
        {
            b.Property<int>("DepartmentID")
                .ValueGeneratedOnAdd()
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
    
            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");
        });
    

搭建“院系”页面的基架

生成项目。

更新“索引”页

基架工具为“索引”页创建了 RowVersion 列,但生产应用中不会显示该字段。 本教程中显示 RowVersion 的最后一个字节,以帮助展示并发处理的工作原理。 无法保证最后一个字节本身是唯一的。

更新 Pages\Departments\Index.cshtml 页:

  • 用院系替换索引。
  • 更改包含 RowVersion 的代码,以便只显示字节数组的最后一个字节。
  • 将 FirstMidName 替换为 FullName。

以下代码显示更新后的页面:

@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>

更新编辑页模型

使用以下代码更新 Pages\Departments\Edit.cshtml.cs :

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);

            if (departmentToUpdate == null)
            {
                return HandleDeletedDepartment();
            }

            _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;
                    // Clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

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

            return Page();
        }

        private IActionResult HandleDeletedDepartment()
        {
            var 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.");
        }
    }
}

OnGet 方法中提取 OriginalValue 时,该值使用实体中的 rowVersion 值更新。 EF Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 命令。 如果没有行受到 UPDATE 命令影响(没有行具有原始 RowVersion 值),将引发 DbUpdateConcurrencyException 异常。

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);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

在上述突出显示的代码中:

  • Department.RowVersion 中的值是最初在“编辑”页的 Get 请求中所提取的实体中的值。 通过 Razor 页面中显示将要编辑的实体的隐藏字段将该值提供给 OnPost 方法。 模型绑定器将隐藏字段值复制到 Department.RowVersion
  • OriginalValue 是 EF Core 将用于 Where 子句的值。 在执行突出显示的代码行之前,OriginalValue 具有在此方法中调用 FirstOrDefaultAsync 时数据库中的值,该值可能与“编辑”页面上所显示的值不同。
  • 突出显示的代码可确保 EF Core 使用原始 RowVersion 值,该值来自于 SQL UPDATE 语句的 Where 子句中所显示的 Department 实体。

发生并发错误时,以下突出显示的代码会获取客户端值(发布到此方法的值)和数据库值。

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;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

以下代码为每列添加自定义错误消息,这些列中的数据库值与发布到 OnPostAsync 的值不同:

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 值设置为从数据库检索的新值。 用户下次单击“保存”时,将仅捕获最后一次显示编辑页后发生的并发错误 。

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;
        // Clear the model error for the next postback.
        ModelState.Remove("Department.RowVersion");
    }

ModelState 具有旧的 RowVersion 值,因此需使用 ModelState.Remove 语句。 在 Razor 页面中,当两者都存在时,字段的 ModelState 值优于模型属性值。

更新 Razor 页面

使用以下代码更新 Pages/Departments/Edit.cshtml :

@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-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

前面的代码:

  • page 指令从 @page 更新为 @page "{id:int}"
  • 添加隐藏的行版本。 必须添加 RowVersion,以便回发绑定值。
  • 显示 RowVersion 的最后一个字节以进行调试。
  • ViewData 替换为强类型 InstructorNameSL

使用编辑页测试并发冲突

在英语系打开编辑的两个浏览器实例:

  • 运行应用,然后选择“院系”。
  • 右键单击英语系的“编辑”超链接,然后选择“在新选项卡中打开” 。
  • 在第一个选项卡中,单击英语系的“编辑”超链接 。

两个浏览器选项卡显示相同信息。

在第一个浏览器选项卡中更改名称,然后单击“保存” 。

更改后的“院系编辑”页 1

浏览器显示更改值并更新 rowVersion 标记后的索引页。 请注意更新后的 rowVersion 标记,它在其他选项卡的第二回发中显示。

在第二个浏览器选项卡中更改不同字段。

更改后的“院系编辑”页 2

单击“保存” 。 可看见所有不匹配数据库值的字段的错误消息:

“院系编辑”页错误消息

此浏览器窗口将不会更改名称字段。 将当前值(语言)复制并粘贴到名称字段。 退出选项卡。客户端验证将删除错误消息。

再次单击“保存” 。 保存在第二个浏览器选项卡中输入的值。 在索引页中可以看到保存的值。

更新“删除”页

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

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 });
            }
        }
    }
}

删除页检测提取实体并更改时的并发冲突。 提取实体后,Department.RowVersion 为行版本。 EF Core 创建 SQL DELETE 命令时,它包括具有 RowVersion 的 WHERE 子句。 如果 SQL DELETE 命令导致零行受影响:

  • SQL DELETE 命令中的 RowVersion 与数据库中的 RowVersion 不匹配。
  • 引发 DbUpdateConcurrencyException 异常。
  • 使用 concurrencyError 调用 OnGetAsync

更新“删除”Razor 页面

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

@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-danger" /> |
            <a asp-page="./Index">Back to List</a>
        </div>
</form>
</div>

上面的代码执行以下更改:

  • page 指令从 @page 更新为 @page "{id:int}"
  • 添加错误消息。
  • 将“管理员”字段中的 FirstMidName 替换为 FullName 。
  • 更改 RowVersion 以显示最后一个字节。
  • 添加隐藏的行版本。 必须添加 RowVersion,以便回发绑定值。

测试并发冲突

创建测试系。

在测试系打开删除的两个浏览器实例:

  • 运行应用,然后选择“院系”。
  • 右键单击测试系的“删除”超链接,然后选择“在新选项卡中打开” 。
  • 单击测试系的“编辑”超链接 。

两个浏览器选项卡显示相同信息。

在第一个浏览器选项卡中更改预算,然后单击“保存” 。

浏览器显示更改值并更新 rowVersion 标记后的索引页。 请注意更新后的 rowVersion 标记,它在其他选项卡的第二回发中显示。

从第二个选项卡中删除测试部门。并发错误显示来自数据库的当前值。 单击“删除”将删除实体,除非 RowVersion 已更新,院系已删除 。

其他资源

后续步骤

这是本系列的最后一个教程。 本系列教程的 MVC 版本中介绍了其他主题。