用ASP.NET Core MVC 和 EF Core 构建Web应用 (二)
本节学习如何执行基本的 CRUD (创建、 读取、 更新、 删除) 操作。
自定义“详细信息”页
学生索引页的基架代码省略了 Enrollments
属性,因为该属性包含一个集合。 在“详细信息”页上,将以 HTML 表形式显示集合的内容。
在 Controllers/StudentsController.cs 中,“详细信息”视图的操作方法使用 SingleOrDefaultAsync
方法检索单个 Student
实体。 添加调用 Include
的代码。 ThenInclude
和 AsNoTracking
方法,如以下突出显示的代码所示。
1 public async Task<IActionResult> Details(int? id) 2 { 3 if (id == null) 4 { 5 return NotFound(); 6 } 7 8 var student = await _context.Students 9 .Include(s => s.Enrollments) 10 .ThenInclude(e => e.Course) 11 .AsNoTracking() 12 .SingleOrDefaultAsync(m => m.ID == id); 13 14 if (student == null) 15 { 16 return NotFound(); 17 } 18 19 return View(student); 20 }
Include
和 ThenInclude
方法使上下文加载 Student.Enrollments
导航属性,并在每个注册中加载 Enrollment.Course
导航属性。
对于返回的实体未在当前上下文生存期中更新的情况,AsNoTracking
方法将会提升性能。
将注册添加到“详细信息”视图
打开 Views/Students/Details.cshtml。 每个字段都使用 DisplayNameFor
和 DisplayFor
帮助器来显示,如下面的示例中所示:
<dt> @Html.DisplayNameFor(model => model.LastName) </dt> <dd> @Html.DisplayFor(model => model.LastName) </dd>
在最后一个字段之后和 </dl>
闭合标记之前,添加以下代码以显示注册列表:
<dt> @Html.DisplayNameFor(model => model.Enrollments) </dt> <dd> <table class="table"> <tr> <th>Course Title</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @Html.DisplayFor(modelItem => item.Course.Title) </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> </dd>
此代码循环通过 Enrollments
导航属性中的实体。 它将针对每个注册显示课程标题和成绩。 课程标题从 Course 实体中检索,该实体存储在 Enrollments 实体的 Course
导航属性中。
运行应用,选择“学生”选项卡,然后单击学生的“详细信息”链接。 将看到所选学生的课程和年级列表。
更新“创建”页
在 StudentsController.cs 中修改 HttpPost Create
方法,在 Bind
特性中添加 try catch 块并删除 ID 值。
1 [HttpPost] 2 [ValidateAntiForgeryToken] 3 public async Task<IActionResult> Create( 4 [Bind("EnrollmentDate,FirstMidName,LastName")] Student student) 5 { 6 try 7 { 8 if (ModelState.IsValid) 9 { 10 _context.Add(student); 11 await _context.SaveChangesAsync(); 12 return RedirectToAction(nameof(Index)); 13 } 14 } 15 catch (DbUpdateException /* ex */) 16 { 17 //Log the error (uncomment ex variable name and write a log. 18 ModelState.AddModelError("", "Unable to save changes. " + 19 "Try again, and if the problem persists " + 20 "see your system administrator."); 21 } 22 return View(student); 23 }
此代码将 ASP.NET MVC 模型绑定器创建的 Student 实体添加到 Students 实体集,然后将更改保存到数据库。(模型绑定器指的是 ASP.NET MVC 功能,用户可利用它来轻松处理使用表单提交的数据;模型绑定器将已发布的表单值转换为 CLR 类型,并将其传递给操作方法的参数。 在本例中,模型绑定器将使用 Form 集合的属性值实例化 Student 实体。)
已从 Bind
特性删除 ID
,因为 ID 是插入行时 SQL Server 将自动设置的主键值。 来自用户的输入不会设置 ID 值。
ValidateAntiForgeryToken
特性帮助抵御跨网站请求伪造 (CSRF) 攻击。 令牌通过 FormTagHelper 自动注入到视图中,并在用户提交表单时包含该令牌。 令牌由 ValidateAntiForgeryToken
特性验证。
运行应用,选择“学生”选项卡,并单击“新建”。
输入姓名和日期。 如果浏览器允许输入无效日期,请尝试输入。 然后单击“创建”,查看错误消息。
将日期更改为有效值,并单击“创建”,查看“索引”页中显示的新学生。
更新“编辑”页
在 StudentController.cs 中,HttpGet Edit
方法(不具有 HttpPost
特性)使用 SingleOrDefaultAsync
方法检索所选的 Student 实体,如 Details
方法中所示。 不需要更改此方法。
使用以下代码替换 HttpPost Edit 操作方法。
1 [HttpPost, ActionName("Edit")] 2 [ValidateAntiForgeryToken] 3 public async Task<IActionResult> EditPost(int? id) 4 { 5 if (id == null) 6 { 7 return NotFound(); 8 } 9 var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id); 10 if (await TryUpdateModelAsync<Student>( 11 studentToUpdate, 12 "", 13 s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)) 14 { 15 try 16 { 17 await _context.SaveChangesAsync(); 18 return RedirectToAction(nameof(Index)); 19 } 20 catch (DbUpdateException /* ex */) 21 { 22 //Log the error (uncomment ex variable name and write a log.) 23 ModelState.AddModelError("", "Unable to save changes. " + 24 "Try again, and if the problem persists, " + 25 "see your system administrator."); 26 } 27 } 28 return View(studentToUpdate); 29 }
这些更改实现安全最佳做法,防止过多发布。 基架生成了 Bind
特性,并将模型绑定器创建的实体添加到具有 Modified
标记的实体集。 不建议将该代码用于多个方案,因为 Bind
特性将清除未在 Include
参数中列出的字段中的任何以前存在的数据。
新代码读取现有实体并调用 TryUpdateModel
,以基于已发布表单数据中的用户输入更新已检索实体中的字段。 Entity Framework 的自动更改跟踪在由表单输入更改的字段上设置 Modified
标记。 调用 SaveChanges
方法时,Entity Framework 会创建 SQL 语句,以更新数据库行。 忽略并发冲突,并且仅在数据库中更新由用户更新的表列。
作为防止过多发布的最佳做法,请将希望通过“编辑”页更新的字段列入 TryUpdateModel
参数。 (参数列表中字段列表之前的空字符串用于与表单字段名称一起使用的前缀。)目前没有要保护的额外字段,但是列出希望模型绑定器绑定的字段可确保以后将字段添加到数据模型时,它们将自动受到保护,直到明确将其添加到此处为止。
这些更改会导致 HttpPost Edit
方法与 HttpGet Edit
方法的方法签名相同,因此已重命名 EditPost
方法。
运行应用,选择“学生”选项卡,然后单击“编辑”超链接。
更改某些数据并单击“保存”。 将打开“索引”页,将看到已更改的数据。
更新“删除”页
在 StudentController.cs 中,HttpGet Delete
方法的模板代码使用 SingleOrDefaultAsync
方法来检索所选的 Student 实体,如 Details 和 Edit 方法中所示。 但是,若要在调用 SaveChanges
失败时实现自定义错误消息,请将部分功能添加到此方法及其相应的视图中。
正如所看到的更新和创建操作,删除操作需要两个操作方法。 为响应 GET 请求而调用的方法将显示一个视图,使用户有机会批准或取消操作。 如果用户批准,则创建 POST 请求。 发生此情况时,将调用 HttpPost Delete
方法,然后该方法实际执行删除操作。
将 try-catch 块添加到 HttpPost Delete
方法,以处理更新数据库时可能出现的任何错误。 如果发生错误,HttpPost Delete 方法会调用 HttpGet Delete 方法,并向其传递一个指示发生错误的参数。 然后 HttpGet Delete 方法重新显示确认页以及错误消息,向用户提供取消或重试的机会。
使用以下管理错误报告的代码替换 HttpGet Delete
操作方法。
1 public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false) 2 { 3 if (id == null) 4 { 5 return NotFound(); 6 } 7 8 var student = await _context.Students 9 .AsNoTracking() 10 .SingleOrDefaultAsync(m => m.ID == id); 11 if (student == null) 12 { 13 return NotFound(); 14 } 15 16 if (saveChangesError.GetValueOrDefault()) 17 { 18 ViewData["ErrorMessage"] = 19 "Delete failed. Try again, and if the problem persists " + 20 "see your system administrator."; 21 } 22 23 return View(student); 24 }
此代码接受可选参数,指示保存更改失败后是否调用此方法。 没有失败的情况下调用 HttpGet Delete
方法时,此参数为 false。 由 HttpPost Delete
方法调用以响应数据库更新错误时,此参数为 true,并且将错误消息传递到视图。
HttpPost Delete 的读取优先方法
使用以下执行实际删除操作并捕获任何数据库更新错误的代码替换 HttpPost Delete
操作方法(名为 DeleteConfirmed
)。
1 [HttpPost, ActionName("Delete")] 2 [ValidateAntiForgeryToken] 3 public async Task<IActionResult> DeleteConfirmed(int id) 4 { 5 var student = await _context.Students 6 .AsNoTracking() 7 .SingleOrDefaultAsync(m => m.ID == id); 8 if (student == null) 9 { 10 return RedirectToAction(nameof(Index)); 11 } 12 13 try 14 { 15 _context.Students.Remove(student); 16 await _context.SaveChangesAsync(); 17 return RedirectToAction(nameof(Index)); 18 } 19 catch (DbUpdateException /* ex */) 20 { 21 //Log the error (uncomment ex variable name and write a log.) 22 return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true }); 23 } 24 }
此代码检索所选的实体,然后调用 Remove
方法以将实体的状态设置为 Deleted
。 调用 SaveChanges
时生成 SQL DELETE 命令。
更新“删除”视图
在 Views/Student/Delete.cshtml 中,在 H2 标题和 H3 标题之间添加错误消息,如以下示例所示:
<h2>Delete</h2> <p class="text-danger">@ViewData["ErrorMessage"]</p> <h3>Are you sure you want to delete this?</h3>
运行应用,选择“学生”选项卡,并单击“删除”超链接:
单击“删除”。 将显示不含已删除学生的索引页。
关闭数据库连接
若要释放数据库连接包含的资源,完成此操作时必须尽快处理上下文实例。 ASP.NET Core 内置依赖关系注入会完成此任务。
在 Startup.cs 中,调用 AddDbContext 扩展方法来预配 ASP.NET DI 容器的 DbContext
类。 默认情况下,该方法将服务生存期设置为 Scoped
。 Scoped
表示上下文对象生存期与 Web 请求生存期一致,并在 Web 请求结束时将自动调用 Dispose
方法。
处理事务
默认情况下,Entity Framework 隐式实现事务。 在对多个行或表进行更改并调用 SaveChanges
的情况下,Entity Framework 自动确保所有更改都成功或全部失败。 如果完成某些更改后发生错误,这些更改会自动回退。
非跟踪查询
当数据库上下文检索表行并创建表示它们的实体对象时,默认情况下,它会跟踪内存中的实体是否与数据库中的内容同步。 更新实体时,内存中的数据充当缓存并使用该数据。 在 Web 应用程序中,此缓存通常是不必要的,因为上下文实例通常生存期较短(创建新的实例并用于处理每个请求),并且通常在再次使用该实体之前处理读取实体的上下文。
可以通过调用 AsNoTracking
方法禁用对内存中的实体对象的跟踪。 可能想要执行的典型方案包括以下操作:
-
在上下文生存期内,不需要更新任何实体,并且不需要 EF 自动加载具有由单独的查询检索的实体的导航属性。 在控制器的 HttpGet 操作方法中经常遇到这些情况。
-
正在运行检索大量数据的查询,将只更新一小部分返回的数据。 关闭对大型查询的跟踪可能更有效,稍后为少数需要更新的实体运行查询。
-
想要附加一个实体来更新它,但之前为了其他目的,已检索了相同的实体。 由于数据库上下文已跟踪了该实体,因此无法附加要更改的实体。 处理这种情况的一种方法是在早前的查询上调用
AsNoTracking
。
总结
现在拥有一组完整的页面,可对 Student 实体执行简单的 CRUD 操作。