ASP.NET Core 中的 Razor 页面和 EF Core - CRUD - 第 2 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - CRUD - 2 of 8

本文内容

作者:Tom DykstraJon P SmithRick Anderson

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

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

本教程将介绍和自定义已搭建基架的 CRUD (创建、读取、更新、删除)代码。

无存储库No repository

某些开发人员使用服务层或存储库模式在 UI (Razor Pages) 和数据访问层之间创建抽象层。本教程不会这样做。为最大程度降低复杂性并让本教程重点介绍 EF Core,将直接在页面模型类中添加 EF Core 代码。

更新“详细信息”页Update the Details page

“学生”页的基架代码不包括注册数据。本部分将向“详细信息”页添加注册。

读取注册Read enrollments

为了在页面上显示学生的注册数据,你需要读取这些数据。Pages/Students/Details.cshtml.cs 中的基架代码仅读取学生数据,但不读取注册数据 :

  1. public async Task<IActionResult> OnGetAsync(int? id)
  2. {
  3. if (id == null)
  4. {
  5. return NotFound();
  6. }
  7. Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);
  8. if (Student == null)
  9. {
  10. return NotFound();
  11. }
  12. return Page();
  13. }

使用以下代码替换 OnGetAsync 方法以读取所选学生的注册数据。突出显示所作更改。

  1. public async Task<IActionResult> OnGetAsync(int? id)
  2. {
  3. if (id == null)
  4. {
  5. return NotFound();
  6. }
  7. Student = await _context.Students
  8. .Include(s => s.Enrollments)
  9. .ThenInclude(e => e.Course)
  10. .AsNoTracking()
  11. .FirstOrDefaultAsync(m => m.ID == id);
  12. if (Student == null)
  13. {
  14. return NotFound();
  15. }
  16. return Page();
  17. }

IncludeThenInclude 方法使上下文加载 Student.Enrollments 导航属性,并在每个注册中加载 Enrollment.Course 导航属性。这些方法将在与数据读取相关的教程中进行详细介绍。

对于返回的实体未在当前上下文中更新的情况,AsNoTracking 方法将会提升性能。AsNoTracking 将在本教程的后续部分中讨论。

显示注册Display enrollments

使用以下代码替换 Pages/Students/Details.cshtml 中的代码以显示注册列表。突出显示所作更改。

  1. @page
  2. @model ContosoUniversity.Pages.Students.DetailsModel
  3. @{
  4. ViewData["Title"] = "Details";
  5. }
  6. <h1>Details</h1>
  7. <div>
  8. <h4>Student</h4>
  9. <hr />
  10. <dl class="row">
  11. <dt class="col-sm-2">
  12. @Html.DisplayNameFor(model => model.Student.LastName)
  13. </dt>
  14. <dd class="col-sm-10">
  15. @Html.DisplayFor(model => model.Student.LastName)
  16. </dd>
  17. <dt class="col-sm-2">
  18. @Html.DisplayNameFor(model => model.Student.FirstMidName)
  19. </dt>
  20. <dd class="col-sm-10">
  21. @Html.DisplayFor(model => model.Student.FirstMidName)
  22. </dd>
  23. <dt class="col-sm-2">
  24. @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
  25. </dt>
  26. <dd class="col-sm-10">
  27. @Html.DisplayFor(model => model.Student.EnrollmentDate)
  28. </dd>
  29. <dt class="col-sm-2">
  30. @Html.DisplayNameFor(model => model.Student.Enrollments)
  31. </dt>
  32. <dd class="col-sm-10">
  33. <table class="table">
  34. <tr>
  35. <th>Course Title</th>
  36. <th>Grade</th>
  37. </tr>
  38. @foreach (var item in Model.Student.Enrollments)
  39. {
  40. <tr>
  41. <td>
  42. @Html.DisplayFor(modelItem => item.Course.Title)
  43. </td>
  44. <td>
  45. @Html.DisplayFor(modelItem => item.Grade)
  46. </td>
  47. </tr>
  48. }
  49. </table>
  50. </dd>
  51. </dl>
  52. </div>
  53. <div>
  54. <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
  55. <a asp-page="./Index">Back to List</a>
  56. </div>

上面的代码循环通过 Enrollments 导航属性中的实体。它将针对每个注册显示课程标题和成绩。课程标题从 Course 实体中检索,该实体存储在 Enrollments 实体的 Course 导航属性中。

运行应用,选择“学生”选项卡,然后单击学生的“详细信息”链接 。随即显示出所选学生的课程和成绩列表。

读取一个实体的方法Ways to read one entity

生成的代码使用 FirstOrDefaultAsync 读取一个实体。如果未找到任何内容,则此方法返回 NULL;否则,它将返回满足查询筛选条件的第一行。FirstOrDefaultAsync 通常是比以下备选方案更好的选择:

  • SingleOrDefaultAsync - 如果有多个满足查询筛选器的实体,则引发异常。若要确定查询是否可以返回多行,SingleOrDefaultAsync 会尝试提取多个行。如果查询只能返回一个实体,就像它在搜索唯一键时一样,那么该额外工作是不必要的。
  • FindAsync - 查找具有主键 ( PK) 的实体。如果具有 PK 的实体正在由上下文跟踪,会返回该实体且不向数据库发出请求。此方法经过优化,可查找单个实体,但无法通过 FindAsync 调用 Include。如果需要相关数据,FirstOrDefaultAsync 则是更好的选择。

路由数据与查询字符串Route data vs. query string

“详细信息”页的 URL 是 https://localhost:<port>/Students/Details?id=1实体的主键值在查询字符串中。某些开发人员偏向于在路由数据中传递键值:https://localhost:<port>/Students/Details/1有关详细信息,请参阅更新生成的代码

更新“创建”页Update the Create page

“创建”页面的基架 OnPostAsync 代码容易受到过多发布攻击使用以下代码替换 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法 。

  1. public async Task<IActionResult> OnPostAsync()
  2. {
  3. var emptyStudent = new Student();
  4. if (await TryUpdateModelAsync<Student>(
  5. emptyStudent,
  6. "student", // Prefix for form value.
  7. s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
  8. {
  9. _context.Students.Add(emptyStudent);
  10. await _context.SaveChangesAsync();
  11. return RedirectToPage("./Index");
  12. }
  13. return Page();
  14. }

TryUpdateModelAsyncTryUpdateModelAsync

前面的代码将创建一个 Student 对象,然后使用发布的表单域更新 Student 对象的属性。TryUpdateModelAsync 方法:

  • 使用 PageModelPageContext 属性的已发布的表单值。
  • 仅更新列出的属性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • 查找带有“student”前缀的表单值。例如 Student.FirstMidName。该自变量不区分大小写。
  • 使用模型绑定系统将字符串中的表单值转换为 Student 模型中的类型。例如,EnrollmentDate 必须转换为 DateTime。

运行应用,并创建一个学生实体以测试“创建”页。

过多发布Overposting

使用 TryUpdateModel 更新具有已发布值的字段是一种最佳的安全做法,因为这能阻止过多发布。例如,假设 Student 实体包含此网页不应更新或添加的 Secret 属性:

  1. public class Student
  2. {
  3. public int ID { get; set; }
  4. public string LastName { get; set; }
  5. public string FirstMidName { get; set; }
  6. public DateTime EnrollmentDate { get; set; }
  7. public string Secret { get; set; }
  8. }

即使应用的创建或更新 Razor 页面上没有 Secret 字段,黑客仍可利用过多发布设置 Secret 值。黑客也可使用 Fiddler 等工具或通过编写某个 JavaScript 来发布 Secret 表单值。原始代码不会限制模型绑定器在创建“学生”实例时使用的字段。

黑客为 Secret 表单域指定的任何值都会在数据库中更新。下图显示 Fiddler 工具正在将 Secret 字段(值为“OverPost”)添加到已发布的表单值。

Fiddler 添加 Secret 字段

值“OverPost”已成功添加到所插入行的 Secret 属性中。即使应用设计器从未打算用“创建”页设置 Secret 属性,也会发生这种情况。

视图模型View model

视图模型还提供了一种防止过度发布的方法。

应用程序模型通常称为域模型。域模型通常包含数据库中对应实体所需的全部属性。视图模型只包含它所用于的 UI(例如,“创建”页)所需的属性。

除视图模型外,某些应用使用绑定模型或输入模型在“Razor 页面”页面模型类和浏览器之间传递数据。

请考虑以下 Student 视图模型:

  1. using System;
  2. namespace ContosoUniversity.Models
  3. {
  4. public class StudentVM
  5. {
  6. public int ID { get; set; }
  7. public string LastName { get; set; }
  8. public string FirstMidName { get; set; }
  9. public DateTime EnrollmentDate { get; set; }
  10. }
  11. }

以下代码使用 StudentVM 视图模型创建新的学生:

  1. [BindProperty]
  2. public StudentVM StudentVM { get; set; }
  3. public async Task<IActionResult> OnPostAsync()
  4. {
  5. if (!ModelState.IsValid)
  6. {
  7. return Page();
  8. }
  9. var entry = _context.Add(new Student());
  10. entry.CurrentValues.SetValues(StudentVM);
  11. await _context.SaveChangesAsync();
  12. return RedirectToPage("./Index");
  13. }

SetValues 方法通过从另一个 PropertyValues 对象读取值来设置此对象的值。SetValues 使用属性名称匹配。视图模型类型不需要与模型类型相关,它只需要具有匹配的属性。

使用 StudentVM 时需要更新 Create.cshtml 才能使用 StudentVM 而非 Student

更新“编辑”页Update the Edit page

在 Pages/Students/Edit.cshtml.cs 中,使用以下代码替换 OnGetAsyncOnPostAsync 方法 。

  1. public async Task<IActionResult> OnGetAsync(int? id)
  2. {
  3. if (id == null)
  4. {
  5. return NotFound();
  6. }
  7. Student = await _context.Students.FindAsync(id);
  8. if (Student == null)
  9. {
  10. return NotFound();
  11. }
  12. return Page();
  13. }
  14. public async Task<IActionResult> OnPostAsync(int id)
  15. {
  16. var studentToUpdate = await _context.Students.FindAsync(id);
  17. if (studentToUpdate == null)
  18. {
  19. return NotFound();
  20. }
  21. if (await TryUpdateModelAsync<Student>(
  22. studentToUpdate,
  23. "student",
  24. s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
  25. {
  26. await _context.SaveChangesAsync();
  27. return RedirectToPage("./Index");
  28. }
  29. return Page();
  30. }

代码更改与“创建”页类似,但有少数例外:

  • 已将 FirstOrDefaultAsync 替换为 FindAsync。不需要包含相关数据时,FindAsync 效率更高。
  • OnPostAsync 具有 id 参数。
  • 当前学生是从数据库中提取的,而非通过创建空学生获得。

运行应用,并通过创建和编辑学生进行测试。

实体状态Entity States

数据库上下文会随时跟踪内存中的实体是否已与其在数据库中的对应行进行同步。此跟踪信息可确定调用 SaveChangesAsync 后的行为。例如,将新实体传递到 AddAsync 方法时,该实体的状态设置为 Added调用 SaveChangesAsync 时,数据库上下文会发出 SQL INSERT 命令。

实体可能处于以下状态之一:

  • Added:数据库中尚不存在实体。SaveChanges 方法发出 INSERT 语句。

  • Unchanged:无需保存对该实体所做的任何更改。从数据库中读取实体时,该实体具有此状态。

  • Modified:已修改实体的部分或全部属性值。SaveChanges 方法发出 UPDATE 语句。

  • Deleted:已标记该实体进行删除。SaveChanges 方法发出 DELETE 语句。

  • Detached:数据库上下文未跟踪该实体。

在桌面应用中,通常会自动设置状态更改。读取实体并执行更改后,实体状态自动更改为 Modified调用 SaveChanges 会生成仅更新已更改属性的 SQL UPDATE 语句。

在 Web 应用中,读取实体并显示数据的 DbContext 将在页面呈现后进行处理。调用页面 OnPostAsync 方法时,将发出具有 DbContext 的新实例的 Web 请求。如果在这个新的上下文中重新读取实体,则会模拟桌面处理。

更新“删除”页Update the Delete page

在此部分中,当对 SaveChanges 的调用失败时,将实现自定义错误消息。

使用以下代码替换 Pages/Students/Delete.cshtml.cs 中的代码。更改将突出显示(而不是清除 using 语句)。

  1. using ContosoUniversity.Models;
  2. using Microsoft.AspNetCore.Mvc;
  3. using Microsoft.AspNetCore.Mvc.RazorPages;
  4. using Microsoft.EntityFrameworkCore;
  5. using System.Threading.Tasks;
  6. namespace ContosoUniversity.Pages.Students
  7. {
  8. public class DeleteModel : PageModel
  9. {
  10. private readonly ContosoUniversity.Data.SchoolContext _context;
  11. public DeleteModel(ContosoUniversity.Data.SchoolContext context)
  12. {
  13. _context = context;
  14. }
  15. [BindProperty]
  16. public Student Student { get; set; }
  17. public string ErrorMessage { get; set; }
  18. public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
  19. {
  20. if (id == null)
  21. {
  22. return NotFound();
  23. }
  24. Student = await _context.Students
  25. .AsNoTracking()
  26. .FirstOrDefaultAsync(m => m.ID == id);
  27. if (Student == null)
  28. {
  29. return NotFound();
  30. }
  31. if (saveChangesError.GetValueOrDefault())
  32. {
  33. ErrorMessage = "Delete failed. Try again";
  34. }
  35. return Page();
  36. }
  37. public async Task<IActionResult> OnPostAsync(int? id)
  38. {
  39. if (id == null)
  40. {
  41. return NotFound();
  42. }
  43. var student = await _context.Students.FindAsync(id);
  44. if (student == null)
  45. {
  46. return NotFound();
  47. }
  48. try
  49. {
  50. _context.Students.Remove(student);
  51. await _context.SaveChangesAsync();
  52. return RedirectToPage("./Index");
  53. }
  54. catch (DbUpdateException /* ex */)
  55. {
  56. //Log the error (uncomment ex variable name and write a log.)
  57. return RedirectToAction("./Delete",
  58. new { id, saveChangesError = true });
  59. }
  60. }
  61. }
  62. }

前面的代码将可选参数 saveChangesError 添加到 OnGetAsync 方法签名中。saveChangesError 指示学生对象删除失败后是否调用该方法。删除操作可能由于暂时性网络问题而失败。数据库在云中时,更可能出现暂时性网络错误。通过 UI 调用“删除”页 OnGetAsync 时,saveChangesError 参数为 false。OnPostAsync 调用 OnGetAsync(由于删除操作失败)时,saveChangesError 参数为 true。

OnPostAsync 方法检索所选实体,然后调用 Remove 方法将实体的状态设置为 Deleted调用 SaveChanges 时生成 SQL DELETE 命令。如果 Remove 失败:

  • 捕获数据库异常。
  • 通过 saveChangesError=true 调用“删除”页 OnGetAsync 方法。

向“删除”Razor 页面添加错误消息 (Pages/Students/Delete.cshtml) :

  1. @page
  2. @model ContosoUniversity.Pages.Students.DeleteModel
  3. @{
  4. ViewData["Title"] = "Delete";
  5. }
  6. <h1>Delete</h1>
  7. <p class="text-danger">@Model.ErrorMessage</p>
  8. <h3>Are you sure you want to delete this?</h3>
  9. <div>
  10. <h4>Student</h4>
  11. <hr />
  12. <dl class="row">
  13. <dt class="col-sm-2">
  14. @Html.DisplayNameFor(model => model.Student.LastName)
  15. </dt>
  16. <dd class="col-sm-10">
  17. @Html.DisplayFor(model => model.Student.LastName)
  18. </dd>
  19. <dt class="col-sm-2">
  20. @Html.DisplayNameFor(model => model.Student.FirstMidName)
  21. </dt>
  22. <dd class="col-sm-10">
  23. @Html.DisplayFor(model => model.Student.FirstMidName)
  24. </dd>
  25. <dt class="col-sm-2">
  26. @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
  27. </dt>
  28. <dd class="col-sm-10">
  29. @Html.DisplayFor(model => model.Student.EnrollmentDate)
  30. </dd>
  31. </dl>
  32. <form method="post">
  33. <input type="hidden" asp-for="Student.ID" />
  34. <input type="submit" value="Delete" class="btn btn-danger" /> |
  35. <a asp-page="./Index">Back to List</a>
  36. </form>
  37. </div>

运行应用并删除学生以测试“删除”页。

后续步骤Next steps

上一个教程下一个教程

本教程将介绍和自定义已搭建基架的 CRUD (创建、读取、更新、删除)代码。

为最大程度降低复杂性并让这些教程集中介绍 EF Core,将在页面模型中使用 EF Core 代码。某些开发人员使用服务层或存储库模式在 UI(Razor 页面)和数据访问层之间创建抽象层。

本教程将检查“学生”文件夹中的“创建”、“编辑”、“删除”和“详细信息”Razor Pages 。

基架代码将以下模式用于“创建”、“编辑”和“删除”页面:

  • 使用 HTTP GET 方法 OnGetAsync 获取和显示请求数据。
  • 使用 HTTP POST 方法 OnPostAsync 将更改保存到数据。

“索引”和“详细信息”页面使用 HTTP GET 方法 OnGetAsync 获取和显示请求数据

SingleOrDefaultAsync 与FirstOrDefaultAsyncSingleOrDefaultAsync vs. FirstOrDefaultAsync

生成的代码使用 FirstOrDefaultAsync其推荐度通常高于 SingleOrDefaultAsync

提取一个实体时,使用 FirstOrDefaultAsync 比使用 SingleOrDefaultAsync 更高效:

  • 代码需要验证查询仅返回一个实体时除外。
  • SingleOrDefaultAsync 会提取更多数据并执行不必要的工作。
  • 如果有多个实体符合筛选部分,SingleOrDefaultAsync 将引发异常。
  • 如果有多个实体符合筛选部分,FirstOrDefaultAsync 不引发异常。

FindAsyncFindAsync

在大部分基架代码中,FindAsync 可用于替代 FirstOrDefaultAsync

FindAsync

  • 查找具有主键 (PK) 的实体。如果具有 PK 的实体正在由上下文跟踪,会返回该实体且不向 DB 发出请求。
  • 既简单又简洁。
  • 经过优化后可查找单个实体。
  • 在某些情况下可以提供性能优势,但很少发生在典型的 Web 应用中。
  • 以隐式方式使用 FirstAsync 而不是 SingleAsync

如果想要 Include 其他实体,则 FindAsync 将不再适用。这意味着可能需要放弃 FindAsync 并随着应用运行移动到查询。

自定义“详细信息”页Customize the Details page

浏览到 Pages/Students 页面。“编辑”、“详细信息”和“删除”链接是在 Pages/Students/Index.cshtml 文件中由定位点标记帮助器生成的 。

  1. <td>
  2. <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
  3. <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
  4. <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
  5. </td>

运行应用并选择“详细信息”链接 。URL 的格式为 http://localhost:5000/Students/Details?id=2“学生 ID”通过查询字符串 (?id=2) 进行传递。

更新“编辑”、“详细信息”和“删除”Razor 页面以使用 "{id:int}" 路由模板。将上述每个页面的页面指令从 @page 更改为 @page "{id:int}"

如果对具有不包含整数路由值的“{id:int}”路由模板的页面发起请求,则该请求将返回 HTTP 404(找不到)错误 。例如,http://localhost:5000/Students/Details 返回 404 错误。若要使 ID 可选,请将 ? 追加到路由约束:

  1. @page "{id:int?}"

运行应用,单击“详细信息”链接,并验证确认 URL 正在将 ID 作为路由数据 (http://localhost:5000/Students/Details/2) 进行传递。

不要将 @page 全局更改为 @page "{id:int}",这样做会破坏指向“主页”和“创建”页面的链接。

添加相关数据Add related data

“学生索引”页的基架代码不包括 Enrollments 属性。在本部分,Enrollments 集合的内容显示在“详细信息”页中。

Pages/Students/Details.cshtml.cs 的 OnGetAsync 方法使用 FirstOrDefaultAsync 方法检索单个 Student 实体 。添加以下突出显示的代码:

  1. public async Task<IActionResult> OnGetAsync(int? id)
  2. {
  3. if (id == null)
  4. {
  5. return NotFound();
  6. }
  7. Student = await _context.Student
  8. .Include(s => s.Enrollments)
  9. .ThenInclude(e => e.Course)
  10. .AsNoTracking()
  11. .FirstOrDefaultAsync(m => m.ID == id);
  12. if (Student == null)
  13. {
  14. return NotFound();
  15. }
  16. return Page();
  17. }

IncludeThenInclude 方法使上下文加载 Student.Enrollments 导航属性,并在每个注册中加载 Enrollment.Course 导航属性。这些方法将在与数据读取相关的教程中进行详细介绍。

对于返回的实体未在当前上下文中更新的情况,AsNoTracking 方法将会提升性能。AsNoTracking 将在本教程的后续部分中讨论。

在“详细信息”页中显示相关注册Display related enrollments on the Details page

打开 Pages/Students/Details.cshtml 。添加以下突出显示的代码以显示注册列表:

  1. @page "{id:int}"
  2. @model ContosoUniversity.Pages.Students.DetailsModel
  3. @{
  4. ViewData["Title"] = "Details";
  5. }
  6. <h2>Details</h2>
  7. <div>
  8. <h4>Student</h4>
  9. <hr />
  10. <dl class="dl-horizontal">
  11. <dt>
  12. @Html.DisplayNameFor(model => model.Student.LastName)
  13. </dt>
  14. <dd>
  15. @Html.DisplayFor(model => model.Student.LastName)
  16. </dd>
  17. <dt>
  18. @Html.DisplayNameFor(model => model.Student.FirstMidName)
  19. </dt>
  20. <dd>
  21. @Html.DisplayFor(model => model.Student.FirstMidName)
  22. </dd>
  23. <dt>
  24. @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
  25. </dt>
  26. <dd>
  27. @Html.DisplayFor(model => model.Student.EnrollmentDate)
  28. </dd>
  29. <dt>
  30. @Html.DisplayNameFor(model => model.Student.Enrollments)
  31. </dt>
  32. <dd>
  33. <table class="table">
  34. <tr>
  35. <th>Course Title</th>
  36. <th>Grade</th>
  37. </tr>
  38. @foreach (var item in Model.Student.Enrollments)
  39. {
  40. <tr>
  41. <td>
  42. @Html.DisplayFor(modelItem => item.Course.Title)
  43. </td>
  44. <td>
  45. @Html.DisplayFor(modelItem => item.Grade)
  46. </td>
  47. </tr>
  48. }
  49. </table>
  50. </dd>
  51. </dl>
  52. </div>
  53. <div>
  54. <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
  55. <a asp-page="./Index">Back to List</a>
  56. </div>

如果代码缩进在粘贴代码后出现错误,请按 CTRL-K-D 进行更正。

上面的代码循环通过 Enrollments 导航属性中的实体。它将针对每个注册显示课程标题和成绩。课程标题从 Course 实体中检索,该实体存储在 Enrollments 实体的 Course 导航属性中。

运行应用,选择“学生”选项卡,然后单击学生的“详细信息”链接 。随即显示出所选学生的课程和成绩列表。

更新“创建”页Update the Create page

将 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法更新为以下代码 :

  1. public async Task<IActionResult> OnPostAsync()
  2. {
  3. if (!ModelState.IsValid)
  4. {
  5. return Page();
  6. }
  7. var emptyStudent = new Student();
  8. if (await TryUpdateModelAsync<Student>(
  9. emptyStudent,
  10. "student", // Prefix for form value.
  11. s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
  12. {
  13. _context.Student.Add(emptyStudent);
  14. await _context.SaveChangesAsync();
  15. return RedirectToPage("./Index");
  16. }
  17. return null;
  18. }

TryUpdateModelAsyncTryUpdateModelAsync

检查 TryUpdateModelAsync 代码:

  1. var emptyStudent = new Student();
  2. if (await TryUpdateModelAsync<Student>(
  3. emptyStudent,
  4. "student", // Prefix for form value.
  5. s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
  6. {

在前面的代码中,TryUpdateModelAsync<Student> 尝试使用 PageModelPageContext 属性中已发布的表单值更新 emptyStudent 对象。TryUpdateModelAsync 仅更新列出的属性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。

在上述示例中:

  • 第二个自变量 ("student", // Prefix) 是用于查找值的前缀。该自变量不区分大小写。
  • 已发布的表单值通过模型绑定转换为 Student 模型中的类型。

过多发布Overposting

使用 TryUpdateModel 更新具有已发布值的字段是一种最佳的安全做法,因为这能阻止过多发布。例如,假设 Student 实体包含此网页不应更新或添加的 Secret 属性:

  1. public class Student
  2. {
  3. public int ID { get; set; }
  4. public string LastName { get; set; }
  5. public string FirstMidName { get; set; }
  6. public DateTime EnrollmentDate { get; set; }
  7. public string Secret { get; set; }
  8. }

即使应用的创建/更新 Razor 页面上没有 Secret 字段,黑客仍可利用过多发布设置 Secret 值。黑客也可使用 Fiddler 等工具或通过编写某个 JavaScript 来发布 Secret 表单值。原始代码不会限制模型绑定器在创建“学生”实例时使用的字段。

黑客为 Secret 表单字段指定的任何值都会在 DB 中更新。下图显示 Fiddler 工具正在将 Secret 字段(值为“OverPost”)添加到已发布的表单值。

Fiddler 添加 Secret 字段

值“OverPost”已成功添加到所插入行的 Secret 属性中。应用程序设计器绝不会在“创建”页设置 Secret 属性。

视图模型View model

视图模型通常包含应用程序所用的模型中包括的属性的子集。应用程序模型通常称为域模型。域模型通常包含 DB 中对应实体所需的全部属性。视图模型仅包含 UI 层(例如“创建”页)所需的属性。除视图模型外,某些应用使用绑定模型或输入模型在“Razor 页面”页面模型类和浏览器之间传递数据。请考虑以下 Student 视图模型:

  1. using System;
  2. namespace ContosoUniversity.Models
  3. {
  4. public class StudentVM
  5. {
  6. public int ID { get; set; }
  7. public string LastName { get; set; }
  8. public string FirstMidName { get; set; }
  9. public DateTime EnrollmentDate { get; set; }
  10. }
  11. }

视图模型还提供了一种防止过度发布的方法。视图模型仅包含要查看(显示)或更新的属性。

以下代码使用 StudentVM 视图模型创建新的学生:

  1. [BindProperty]
  2. public StudentVM StudentVM { get; set; }
  3. public async Task<IActionResult> OnPostAsync()
  4. {
  5. if (!ModelState.IsValid)
  6. {
  7. return Page();
  8. }
  9. var entry = _context.Add(new Student());
  10. entry.CurrentValues.SetValues(StudentVM);
  11. await _context.SaveChangesAsync();
  12. return RedirectToPage("./Index");
  13. }

SetValues 方法通过从另一个 PropertyValues 对象读取值来设置此对象的值。SetValues 使用属性名称匹配。视图模型类型不需要与模型类型相关,它只需要具有匹配的属性。

使用 StudentVM 时需要更新 CreateVM.cshtml 才能使用 StudentVM 而非 Student

在 Razor 页面,PageModel 派生类就是视图模型。

更新“编辑”页Update the Edit page

更新“编辑”页的页面模型。突出显示所作的主要更改:

  1. public class EditModel : PageModel
  2. {
  3. private readonly SchoolContext _context;
  4. public EditModel(SchoolContext context)
  5. {
  6. _context = context;
  7. }
  8. [BindProperty]
  9. public Student Student { get; set; }
  10. public async Task<IActionResult> OnGetAsync(int? id)
  11. {
  12. if (id == null)
  13. {
  14. return NotFound();
  15. }
  16. Student = await _context.Student.FindAsync(id);
  17. if (Student == null)
  18. {
  19. return NotFound();
  20. }
  21. return Page();
  22. }
  23. public async Task<IActionResult> OnPostAsync(int? id)
  24. {
  25. if (!ModelState.IsValid)
  26. {
  27. return Page();
  28. }
  29. var studentToUpdate = await _context.Student.FindAsync(id);
  30. if (await TryUpdateModelAsync<Student>(
  31. studentToUpdate,
  32. "student",
  33. s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
  34. {
  35. await _context.SaveChangesAsync();
  36. return RedirectToPage("./Index");
  37. }
  38. return Page();
  39. }
  40. }

代码更改与“创建”页类似,但有少数例外:

  • OnPostAsync 具有可选的 id 参数。
  • 当前学生是从 DB 提取的,而非通过创建空学生获得。
  • 已将 FirstOrDefaultAsync 替换为 FindAsync。从主键中选择实体时,使用 FindAsync 是一个不错的选择。请参阅 FindAsync 了解详细信息。

测试“编辑”和“创建”页Test the Edit and Create pages

创建和编辑几个学生实体。

实体状态Entity States

DB 上下文会随时跟踪内存中的实体是否已与其在 DB 中的对应行进行同步。DB 上下文同步信息可决定调用 SaveChangesAsync 后的行为。例如,将新实体传递到 AddAsync 方法时,该实体的状态设置为 Added调用 SaveChangesAsync 时,DB 上下文会发出 SQL INSERT 命令。

实体可能处于以下状态之一:

  • Added:DB 中尚不存在实体。SaveChanges 方法发出 INSERT 语句。

  • Unchanged:无需保存对该实体所做的任何更改。从 DB 中读取实体时,该实体将具有此状态。

  • Modified:已修改实体的部分或全部属性值。SaveChanges 方法发出 UPDATE 语句。

  • Deleted:已标记该实体进行删除。SaveChanges 方法发出 DELETE 语句。

  • Detached:DB 上下文未跟踪该实体。

在桌面应用中,通常会自动设置状态更改。读取实体并执行更改后,实体状态自动更改为 Modified调用 SaveChanges 会生成仅更新已更改属性的 SQL UPDATE 语句。

在 Web 应用中,读取实体并显示数据的 DbContext 将在页面呈现后进行处理。调用页面 OnPostAsync 方法时,将发出具有 DbContext 的新实例的 Web 请求。如果在这个新的上下文中重新读取实体,则会模拟桌面处理。

更新“删除”页Update the Delete page

在此部分中,当对 SaveChanges 的调用失败时,将添加用于实现自定义错误消息的代码。添加字符串,使其包含可能的错误消息:

public class DeleteModel : PageModel
{
    private readonly SchoolContext _context;

    public DeleteModel(SchoolContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Student Student { get; set; }
    public string ErrorMessage { get; set; }

OnGetAsync 方法替换为以下代码:

public async Task<IActionResult> OnGetAsync(int? id, bool? saveChangesError = false)
{
    if (id == null)
    {
        return NotFound();
    }

    Student = await _context.Student
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

    if (saveChangesError.GetValueOrDefault())
    {
        ErrorMessage = "Delete failed. Try again";
    }

    return Page();
}

上述代码包含可选参数 saveChangesErrorsaveChangesError 指示学生对象删除失败后是否调用该方法。删除操作可能由于暂时性网络问题而失败。云端更可能出现暂时性网络错误。通过 UI 调用“删除”页 OnGetAsync 时,saveChangesError 为 false。OnPostAsync 调用 OnGetAsync(由于删除操作失败)时,saveChangesError 参数为 true。

“删除”页 OnPostAsync 方法The Delete pages OnPostAsync method

OnPostAsync 替换为以下代码:

public async Task<IActionResult> OnPostAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var student = await _context.Student
                    .AsNoTracking()
                    .FirstOrDefaultAsync(m => m.ID == id);

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

    try
    {
        _context.Student.Remove(student);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }
    catch (DbUpdateException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction("./Delete",
                             new { id, saveChangesError = true });
    }
}

上述代码检索所选的实体,然后调用 Remove 方法,将实体的状态设置为 Deleted调用 SaveChanges 时生成 SQL DELETE 命令。如果 Remove 失败:

  • 会捕获 DB 异常。
  • 通过 saveChangesError=true 调用“删除”页 OnGetAsync 方法。

更新“删除”Razor 页面Update the Delete Razor Page

将以下突出显示的错误消息添加到“删除”Razor 页面。

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

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

<h2>Delete</h2>

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

<h3>Are you sure you want to delete this?</h3>
<div>

测试“删除”。

常见错误Common errors

“学生/索引”或其他链接不起作用:

验证确认 Razor 页面包含正确的 @page 指令。例如,“学生/索引”Razor Pages 不得 包含路由模板:

@page "{id:int}"

每个 Razor 页面均必须包含 @page 指令。

其他资源Additional resources

上一页下一页