处理并发冲突 - EF Core 与 ASP.NET Core MVC 教程 (8 of 10)
作者 Tom Dykstra 、 Rick Anderson
Contoso 大学 Web应用程序演示了如何使用 Entity Framework Core 1.1 以及 Visual Studio 2017 来创建 ASP.NET Core 1.1 MVC Web 应用程序。更多信息请参考 第一节教程.
在之前的教程中你已经学习了如何更新数据。本教程将指导你当多个用户同时更新同一实体时如何处理冲突。
你将创建与 Department 实体一起使用的页面,并以此页面处理并发错误。下图展示了 Edit 和 Delete 页面,包括发生并发冲突(concurrency conflict)时显示的一些消息。
并发冲突
当某个用户显示实体并打算对其进行编辑,而另一个用户在前一个用户尚未提交更新并写入数据库时更新了相同实体的数据,此时将发生并发冲突(concurrency conflict)。如果不启用检测此类冲突,那么后一次更新数据库将覆盖其他用户的变更。在许多应用程序中,这一风险是可被接受的:他们没有多少用户,或者数据几乎没有更新,又或者对于数据更改被覆盖并不怎么重要,,那么并发编程的成本可能就高于其优势了。在那种情况下,你就不必为应用程序配置处理并发冲突。
悲观并发控制(锁)
如果你的应用程序需要在并发场景下防止意外丢失数据,一种办法时使用数据库锁。这叫做悲观并发(pessimistic concurrency)。比方说在你读取一条数据库记录前,先请求一个只读锁或更新访问锁。,如果你为更新访问锁定一行记录,那么其他用户无论是只读还是更新操作都无法锁定该记录,因为他们将拿到的只是被操作的数据的副本。如果将行锁定为只读访问,那么其他用户也能将该条记录锁定为只读访问,但其他用户不能更新该条记录。
锁的管理是有缺点的,它会导致程序变得复杂。它需要大量的数据库管理资源,并会随应用程序用户量的增加而产生性能问题。正因为如此,并非所有数据库管理系统都支持悲观并发。Entity Framework Core 不为其提供内建支持,本教程也不会向你介绍如何实现它。
乐观并发控制
悲观并发的替代方案时乐观并发(optimistic concurrency)。乐观并发意味着允许并发冲突的发生,并适当做出反应。例如约翰运行 Department 的 Edit 页面,将英语系的预算金额从 $350,000.00 调整为 $0.00。
在约翰点击 Save 前,珍妮运行了相同的页面并将开始时间从 9/1/2007 改为 8/8/2013。
当约翰第一次点击 Save 后将返回 Index 页并看到变更的数据。
而当珍妮在 Edit 页面上点击 Save 时预算仍是 $350,000.00。那么接下来所发生的,就是我们要演示如何处理并发冲突了。
有这么几种方案可供选择:
你可以跟踪用户已修改的属性,并仅更新相应的列到数据库。
在示例场景中,不会丢失任何数据,因为两个用户更新的属性并不相同。下一次某人浏览英语系的时候他就能看到约翰和珍妮所做的修改——开始时间为 8/8/2013,预算为 0 美元。这种更新方法可以减少冲突的发生,但如果对实体的相同属性进行竞争性的更改,则不可避免地会导致数据丢失。Entity Framework 是否以此机制运行取决于其实如何实现代码的。这在 Web 应用程序中通常不实用,因为它可能会需要维持大量的状态以便能跟踪实体的所有属性的旧值和新值。维护大量的状态可能会导致程序的性能问题,因为这需要消费服务器资源,或者必须包含在网页本身(如隐藏字段)或 Cookie 中。
Y你可以让珍妮所做的修改覆盖约翰的修改。
下次某人浏览英语系的时候,他们将看到的是 8/8/2013 和 $350,000.00。这叫做 Client Wins 或 Last in Wins 场景(所有来自客户端的值都优先于数据库中存储的值)。如本节介绍中所述,如果你没有对并发处理进行任何编码,则会自动发生。
你可以组织珍妮所做的修改在数据库更新之后写入数据库。
通常来讲,你会看到一条错误消息,向她显示当前的数据状态并允许他重新应用其修改(如果她仍然想更改的话)。这叫做 Store Wins 场景(数据库存储的值优先于客户端提交的值)。在本教程中你将实现 Store Wins 场景。此方法确保不会覆盖任何变更,且不会向用户发出任何警告。
检测并发冲突
你可以通过处理 Entity Framework 抛出的 DbConcurrencyException
异常来解决冲突。为了知道何时抛出异常,Entity Framework 必须能检测冲突。因此你必须正确配置数据库和数据模型。启用冲突检测有以下选项,包括:
在数据库表中,可以包括一个用于表示本行很是修改过的跟踪列。然后你可以配置 Entity Framework 将该列包含在 SQL Update 或 Delete 的 Where 子句中。
跟踪列的数据类型通常是
rowversion
。rowversion
的值时每次更新行的时候会递增的序列号。在 Update 或 Delete 命令中,Where 子句中包含跟踪列的原始值(原始行的版本)。如果该行已被其他人更新,rowversion
列的值就会与原始之不同,因此 Update 或 Delete 语句就不会找到需要更新的那一行(因为 Where 子句的缘故)。当 Entity Framework 发现更无可更或删无可删的时候(也就是说受影响的行数为零的时候),它将解释其为并发冲突。
配置 Entity Framework,在 Update 或 Delete 的 Where 子句中包含表中每一列的原始值。
与第一个选项一样,如果第一次读取后行中任何内容发生变化,则 Where 子句将不会返回要更新的行,Entity Framework 将解释其为并发冲突。对于具有多行记录的数据库来讲,这个方法可能会导致大体积的 Where 子句,并且可能会需要维护大量状态。如前所述,维护大量状态将导致应用程序的性能问题。因此此方法通常不推荐使用,它也不会在本教程中使用。
如果你想实现这种方法来应对并发,你需要标记需要跟踪并发性的实体内所有非主键属性添加
ConcurrencyCheck
特性。这一改变将使 Entity Framework 能将所有列包含在 Update 和 Delete 语句的 Where 子句之中。
在本教程余下部分中你将会向 Department 实体添加一个 rowversion
跟踪属性,创建一个控制器和视图,并测试验证一下是否工作正常。
在 Department 实体中添加跟踪属性
在 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
特性指定该列将被包含在发送到数据库中的 Update 和 Delete 命令的子句中。该属性叫做 Timestamp
是因为 SQL Server 以前的版本所提供的 Timestamp
数据类型现在已被 rowversion
替代。 rowversion
的 .NET 类型是一个字节数组。
If you prefer to use the fluent API, you can use the IsConcurrencyToken
method (in Data/SchoolContext.cs) to specify the tracking property, as shown in the following example:
如果你倾向于使用 fluent API,可以使用 IsConcurrencyToken
(在 Data/SchoolContext.cs 文件中) 方法来指定跟踪属性,如下例所示:
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
由于你添加了一个属性,改变了数据库模型,因此你需要另一个迁移。
保存变更并构建项目,然后在命令窗口键入下列命令:
dotnet ef migrations add RowVersion
dotnet ef database update
创建 Department 控制器和视图
搭建一个 Departments 控制器和视图,如之前给 Students、Courses 以及 Instructors 创建的那样。
在 DepartmentsController 文件中,将出现的所有四个「FirstMidName」都改为「FullName」,一边系管理员下拉菜单包含教师的完整名称,而不是他们的姓。
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);
更新 Department Index 视图
在 Index 视图中有基架引擎创建的 RowVersion 列。但你想要显示的是 Administrator,而不是什么 RowVersion。
用以下代码替换 Views/Departments/Index.cshtml 中的代码。
@model IEnumerable<ContosoUniversity.Models.Department>
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<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>
<a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
这个改动将会把标题改为「Departments」,重新排列字段,并使用 Administrator 列代替 RowVersion 列。
在 Departments 控制器中更新 Edit 方法
在 HttpGet 的 Edit
方法和 Details
方法中, 添加 AsNoTracking
。 在 HttpGet 的 Edit
方法中,,预加载 Administrator 导航属性。
var department = await _context.Departments
.Include(i => i.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);
用下列代码替换 HttpPost Edit
方法中已有的代码:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
if (id == null)
{
return NotFound();
}
var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).SingleOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
}
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction("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 changes. The department was deleted by another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
}
if (databaseValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
}
if (databaseValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
}
if (databaseValues.InstructorID != clientValues.InstructorID)
{
Instructor databaseInstructor = await _context.Instructors.SingleOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. 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. Otherwise click the Back to List hyperlink.");
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
}
}
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}
代码从尝试读取需要更新的系开始,如果 SingleOrDefaultAsync
方法返回 null,则说明系被另一个用户删除了。那种情形下代码使用 POST 来的值创建一个系实体,以便可以重新展示 Edit 页并显示错误消息。或者,你也可以不重新创建该系实体,只需要显示错误信息而不重新显示该系的字段。
原始的 RowVersion
值存放在视图的隐藏字段中,此方法在 RowVersion
参数中接收该值。在你调用 SaveChanges
之前,你必须在实体的 OriginalValues
集合中放置原始的 RowVersion
属性值
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;
然后当 Entity Framework 创建一个 SQL UPDATE 命令时,该命令包含的 WHERE 子句中就含有查询具有原始 RowVersion
值的行的那一部分。如果没有行受到 UPDATE 命令的影响(没有一行的 RowVersion
值与 WHERE 子句中指示的一致),那么 Entity Framework 将抛出 DbUpdateConcurrencyException
异常。
在该异常的 catch 代码块中,通过异常对象能获取受影响的 Department 实体,该实体的 Entries
属性能获得更新后的值。
var exceptionEntry = ex.Entries.Single();
Entries
集合只有一个 EntityEntry
对象,且该对象具有用户输入的新值。
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
代码为每一个在页面(用户编辑过的 Edit 页面)上的值与数据库中的值不同的列添加一个定制错误信息(为简洁起见,此处仅显示一个字段)。
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
最后,代码将 departmentToUpdate
的 RowVersion
值设置为来自数据库的新值。当重新显示编辑页面时,新的 RowVersion
值将保存在重新显示后的 Edit 页的隐藏字段中,并在下一次用户点击 Save 时只捕获自重新显示 Edit 页面以来的并发错误。
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
ModelState.Remove
语句是非常必须的,因为 ModelState
中具有旧的 RowVersion
值。在视图中,当两个字段都存在时,字段 ModelState
值优先于模型属性的值
更新 Department Edit 视图
在 Views/Departments/Edit.cshtml 中作如下修改:
- 移除支持
RowVersion
字段的<div>
元素。
- 添加一个用于保存
RowVersion
属性值的隐藏字段,紧跟在DepartmentID
属性的隐藏字段之后。
- 在下拉菜单中添加「Select Administrator」选项。
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<form asp-action="Edit">
<div class="form-horizontal">
<h4>Department</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Budget" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="StartDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label col-md-2"></label>
<div class="col-md-10">
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
在 Edit 页面中测试并发冲突
运行站点,点击 Departments 跳转到 Departments Index 页。
右键点击英语系的 Edit 超链并选择 Open in new tab,然后点击英语系的 Edit 超链。现在两个浏览器标签页都显示了相同的页面。
在第一个浏览器标签页中修改字段,并点击 Save。
浏览器会显示更新数据后的 Index 页。
修改第二个浏览器标签页中的字段。
点击 Save。然后你能看到如下错误信息:
再次点击 Save。第二个浏览器标签页中输入的值将与你第一个浏览器标签页中修改前的原始值一道被保存起来。当显示 Index 页的时候,你可以看到保存后的值。
更新 Delete 页面
对于 Delete 页面,Entity Framework 检测由其他人以类似方法编辑系引发的并发冲突。当 HttpGet Delete
方法显示确认视图,视图将原始 RowVersion
值包含在隐藏字段中。然后当用户确认删除时,该值可用于调用 HttpPost Delete
方法。当 Entity Framework 创建 SQL DELETE 命令时,它将带有包含原始 RowVersion
值的 Where 子句。如果该命令影响的行数(表示删除确认后页面中改变的行数)是零,则会抛出并发异常,并以一个置为 true 的错误标识符调用 HttpGet Delete
方法,以便能重新显示带有错误消息的确认页面。零行受到影响也有可能是该条记录被其它用户删除了,因此在这种情况下不会显示错误消息。
在 Departments 控制器中更新 Delete 方法
在 DepartmentController.cs 中用下列代码代替 HttpGet Delete
方法:
public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
if (id == null)
{
return NotFound();
}
var department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction("Index");
}
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "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. Otherwise "
+ "click the Back to List hyperlink.";
}
return View(department);
}
该方法接受一个可选的参数,这个可选参数用于指示并发错误发生后是否正常重新显示页面。如果此标志是 true,则通过使用 ViewData
将错误消息发送到视图中。
用下列代码替换 HttpPost Delete
方法(名为 DeleteConfirmed
)中的代码:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction("Delete", new { concurrencyError = true, id = department.DepartmentID });
}
}
在你先前所替换的基架代码中,这个方法仅接受一个记录 ID:
public async Task<IActionResult> DeleteConfirmed(int id)
你已将此参数修改为模型绑定器创建的 Department 实体实例。这将允许 EF 访问除记录键以外的 RowVersion 属性值
public async Task<IActionResult> Delete(Department department)
你也把操作方法的名称从 DeleteConfirmed
改成了 Delete
。基架代码中使用 DeleteConfirmed
这个名字给 HttpPost 方法一个独有的方法签名(CLR 要求重载方法具有不同的方法参数)。现在签名已经唯一了,你可以依据 MVC 的约定,为 HttpPost 和 HttpGet 的删除方法使用相同的方法名。
如果 department 已被删除, AnyAsync
方法会返回 false,然后应用程序返回到 Index 方法。
如果捕获到并发错误,该代码将重新显示 Delete 确认页面,并提供一个表示应当显示并发错误消息的标志。
更新 Delete 视图
用以下添加有错误信息字段和 DepartmentID 与 RowVersion 属性隐藏字段的代码替换 Views/Department/Delete.cshtml 中的代码。变化部分已被高亮显示。
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@ViewData["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.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
它将发生如下变化:
- 在
h2
和h3
标题之间添加错误消息。
- 在 Administrator 字段中用 FullName 替换 LastName。
- 移除 RowVersion 字段。
- 为
DepartmentID
和RowVersion
属性添加隐藏字段。
运行 Departments Index 页。右键点击英语系的 Delete 超链并选择 Open in new tab,然后再在第一个标签页中点击英语系的 Edit 超链。
在第一个窗口中,随便选择一个值,修改一下,然后点击 Save:
在第二个标签页中,点击 Delete。你能看到一个并发错误消息,以及 Department 的值刷新为当前数据库中的值。
如果你再次点击 Delete,你会被重新定向到 Index 页,你能在 Index 上看到该系已经被删除。
更新 Details 和 Create 视图
你可以选择在 Details 和 Create 视图中清理基架代码。
修改 Views/Departments/Details.cshtml 中的代码,将 RowVersion 列改为 Administrator 列。
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
修改 Views/Departments/Create.cshtml 中的代码,在下拉菜单中添加一个选择项。
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<form asp-action="Create">
<div class="form-horizontal">
<h4>Department</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Budget" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="StartDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="col-md-2 control-label"></label>
<div class="col-md-10">
<select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
总结
至此完成了对处理并发异常的介绍。更多有关 EF Core 中如何处理异常的资料可以阅读 并发冲突。下一篇教程将介绍如何为 Stdent 和 Instructor 实体实现表层次结构继承。