- EF Core 和 Razor Pages
- 使用 MVC 的 EF Core
- Visual Studio的Azure存储
ASP.NET Core 中的 Razor 页面和 EF Core - 并发 - 第 8 个教程(共 8 个)
作者:Rick Anderson、Tom Dykstra 和 Jon 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 。
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();
-
Visual Studio
对于 SQL Server 数据库,定义为字节数组的实体属性上的
[Timestamp]
特性:- 使列包含在 DELETE 和 UPDATE WHERE 子句中。
- 将数据库中的列类型设置为 rowversion。
数据库生成有序的行版本号,该版本号随着每次行的更新递增。 在
Update
或Delete
命令中,Where
子句包括提取的行版本值。 如果要更新的行在提取之后已更改:- 当前的行版本值与提取值不相匹配。
Update
或Delete
命令不查找行,因为Where
子句会查找提取行的版本值。- 引发一个
DbUpdateConcurrencyException
。
以下代码显示更新 Department 名称时由 EF Core 生成的部分 T-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
子句。 如果数据库RowVersion
不等于RowVersion
参数 (@p2
),则不更新行。以下突出显示的代码显示验证更新哪一行的 T-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 返回受上一语句影响的行数。 如果没有更新行,EF Core 会引发
DbUpdateConcurrencyException
。 -
Visual Studio Code
对于 SQLite 数据库,定义为字节数组的实体属性上的
[Timestamp]
特性:- 使列包含在 DELETE 和 UPDATE WHERE 子句中。
- 映射到 BLOB 列类型。
每当行更新时,数据库触发器都会将 RowVersion 列更新为新的随机字节数组。 在
Update
或Delete
命令中,Where
子句包括 RowVersion 列的提取值。 如果要更新的行在提取之后已更改:- 当前的行版本值与提取值不相匹配。
Update
或Delete
命令不查找行,因为Where
子句会查找原始行版本值。- 引发一个
DbUpdateConcurrencyException
。
更新数据库
添加 RowVersion
属性可更改需要迁移的数据库模型。
生成项目。
-
Visual Studio
在 PMC 中运行以下命令:
Add-Migration RowVersion
-
Visual Studio Code
在终端中运行以下命令:
dotnet ef migrations add 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"); });
-
Visual Studio
在 PMC 中运行以下命令:
Update-Database
-
Visual Studio Code
打开
Migrations/<timestamp>_RowVersion.cs
文件,并添加以下突出显示的代码:using System; using Microsoft.EntityFrameworkCore.Migrations; namespace ContosoUniversity.Migrations { public partial class RowVersion : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<byte[]>( name: "RowVersion", table: "Department", rowVersion: true, nullable: true); migrationBuilder.Sql( @" UPDATE Department SET RowVersion = randomblob(8) "); migrationBuilder.Sql( @" CREATE TRIGGER SetRowVersionOnUpdate AFTER UPDATE ON Department BEGIN UPDATE Department SET RowVersion = randomblob(8) WHERE rowid = NEW.rowid; END "); migrationBuilder.Sql( @" CREATE TRIGGER SetRowVersionOnInsert AFTER INSERT ON Department BEGIN UPDATE Department SET RowVersion = randomblob(8) WHERE rowid = NEW.rowid; END "); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "RowVersion", table: "Department"); } } }
前面的代码:
- 将现有行更新为随机 blob 值。
- 添加数据库触发器,该触发器在行更新时将 RowVersion 列设置为随机 blob 值。
在终端中运行以下命令:
dotnet ef database update
搭建“院系”页面的基架
-
Visual Studio
遵循搭建“学生”页的基架中的说明,但以下情况除外:
创建“Pages/Departments”文件夹 。
将
Department
用于模型类。- 使用现有的上下文类,而不是新建上下文类。
-
Visual Studio Code
创建“Pages/Departments”文件夹 。
运行以下命令,搭建“院系”页的基架。
在 Windows 上:
dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages\Departments --referenceScriptLibraries
在 Linux 或 macOS 上:
dotnet aspnet-codegenerator razorpage -m Department -dc SchoolContext -udl -outDir Pages/Departments --referenceScriptLibraries
生成项目。
更新“索引”页
基架工具为“索引”页创建了 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
。
使用编辑页测试并发冲突
在英语系打开编辑的两个浏览器实例:
- 运行应用,然后选择“院系”。
- 右键单击英语系的“编辑”超链接,然后选择“在新选项卡中打开” 。
- 在第一个选项卡中,单击英语系的“编辑”超链接 。
两个浏览器选项卡显示相同信息。
在第一个浏览器选项卡中更改名称,然后单击“保存” 。
浏览器显示更改值并更新 rowVersion 标记后的索引页。 请注意更新后的 rowVersion 标记,它在其他选项卡的第二回发中显示。
在第二个浏览器选项卡中更改不同字段。
单击“保存” 。 可看见所有不匹配数据库值的字段的错误消息:
此浏览器窗口将不会更改名称字段。 将当前值(语言)复制并粘贴到名称字段。 退出选项卡。客户端验证将删除错误消息。
再次单击“保存” 。 保存在第二个浏览器选项卡中输入的值。 在索引页中可以看到保存的值。
更新“删除”页
使用以下代码更新 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 版本中介绍了其他主题。