Contoso University Web 應(yīng)用演示了如何使用 EF Core 和 Visual Studio 創(chuàng)建 Razor 頁面 Web 應(yīng)用。若要了解系列教程,請參閱第一個(gè)教程。
本教程將介紹和自定義已搭建基架的 CRUD (創(chuàng)建、讀取、更新、刪除)代碼。
為最大程度降低復(fù)雜性并讓這些教程集中介紹 EF Core,將在頁面模型中使用 EF Core 代碼。 某些開發(fā)人員使用服務(wù)層或存儲庫模式在 UI(Razor 頁面)和數(shù)據(jù)訪問層之間創(chuàng)建抽象層。
本教程將檢查“學(xué)生”文件夾中的“創(chuàng)建”、“編輯”、“刪除”和“詳細(xì)信息”Razor Pages。
基架代碼將以下模式用于“創(chuàng)建”、“編輯”和“刪除”頁面:
“索引”和“詳細(xì)信息”頁面使用 HTTP GET 方法 OnGetAsync 獲取和顯示請求數(shù)據(jù)
生成的代碼使用 FirstOrDefaultAsync其推薦度通常高于 SingleOrDefaultAsync。
提取一個(gè)實(shí)體時(shí),使用 FirstOrDefaultAsync 比使用 SingleOrDefaultAsync 更高效:
在大部分基架代碼中,FindAsync 可用于替代 FirstOrDefaultAsync。
FindAsync:
如果想要 Include 其他實(shí)體,則 FindAsync 將不再適用。 這意味著可能需要放棄 FindAsync 并隨著應(yīng)用運(yùn)行移動到查詢。
瀏覽到 Pages/Students 頁面。 “編輯”、“詳細(xì)信息”和“刪除”鏈接是在 Pages/Students/Index.cshtml 文件中由定位點(diǎn)標(biāo)記幫助器生成的。
CSHTML
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
運(yùn)行應(yīng)用并選擇“詳細(xì)信息”鏈接。 URL 的格式為 http://localhost:5000/Students/Details?id=2。 “學(xué)生 ID”通過查詢字符串 (?id=2) 進(jìn)行傳遞。
更新“編輯”、“詳細(xì)信息”和“刪除”Razor 頁面以使用 "{id:int}" 路由模板。 將上述每個(gè)頁面的頁面指令從 @page 更改為 @page "{id:int}"。
如果對具有不包含整數(shù)路由值的“{id:int}”路由模板的頁面發(fā)起請求,則該請求將返回 HTTP 404(找不到)錯(cuò)誤。 例如,http://localhost:5000/Students/Details 返回 404 錯(cuò)誤。 若要使 ID 可選,請將 ? 追加到路由約束:
CSHTML
@page "{id:int?}"
運(yùn)行應(yīng)用,單擊“詳細(xì)信息”鏈接,并驗(yàn)證確認(rèn) URL 正在將 ID 作為路由數(shù)據(jù) (http://localhost:5000/Students/Details/2) 進(jìn)行傳遞。
不要將 @page 全局更改為 @page "{id:int}",執(zhí)行此操作會將鏈接拆分為“主頁”和“創(chuàng)建”頁。
“學(xué)生索引”頁的基架代碼不包括 Enrollments 屬性。 在本部分,Enrollments 集合的內(nèi)容顯示在“詳細(xì)信息”頁中。
Pages/Students/Details.cshtml.cs 的 OnGetAsync 方法使用 FirstOrDefaultAsync 方法檢索單個(gè) Student 實(shí)體。 添加以下突出顯示的代碼:
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Student
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
Include 和 ThenInclude 方法使上下文加載 Student.Enrollments 導(dǎo)航屬性,并在每個(gè)注冊中加載 Enrollment.Course 導(dǎo)航屬性。 這些方法將在與數(shù)據(jù)讀取相關(guān)的教程中進(jìn)行詳細(xì)介紹。
對于返回的實(shí)體未在當(dāng)前上下文中更新的情況,AsNoTracking 方法將會提升性能。 AsNoTracking 將在本教程的后續(xù)部分中討論。
打開 Pages/Students/Details.cshtml。 添加以下突出顯示的代碼以顯示注冊列表:
CSHTML
@page "{id:int}"
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd>
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
如果代碼縮進(jìn)在粘貼代碼后出現(xiàn)錯(cuò)誤,請按 CTRL-K-D 進(jìn)行更正。
上面的代碼循環(huán)通過 Enrollments 導(dǎo)航屬性中的實(shí)體。 它將針對每個(gè)注冊顯示課程標(biāo)題和成績。 課程標(biāo)題從 Course 實(shí)體中檢索,該實(shí)體存儲在 Enrollments 實(shí)體的 Course 導(dǎo)航屬性中。
運(yùn)行應(yīng)用,選擇“學(xué)生”選項(xiàng)卡,然后單擊學(xué)生的“詳細(xì)信息”鏈接。 隨即顯示出所選學(xué)生的課程和成績列表。
將 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法更新為以下代碼:
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Student.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return null;
}
檢查 TryUpdateModelAsync 代碼:
C#
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student", // Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
在前面的代碼中,TryUpdateModelAsync<Student> 嘗試使用 PageModel 的 PageContext 屬性中已發(fā)布的表單值更新 emptyStudent 對象。 TryUpdateModelAsync 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
在上述示例中:
使用 TryUpdateModel 更新具有已發(fā)布值的字段是一種最佳的安全做法,因?yàn)檫@能阻止過多發(fā)布。 例如,假設(shè) Student 實(shí)體包含此網(wǎng)頁不應(yīng)更新或添加的 Secret 屬性:
C#
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
即使應(yīng)用的創(chuàng)建/更新 Razor 頁面上沒有 Secret 字段,黑客仍可利用過多發(fā)布設(shè)置 Secret 值。 黑客也可使用 Fiddler 等工具或通過編寫某個(gè) JavaScript 來發(fā)布 Secret 表單值。 原始代碼不會限制模型綁定器在創(chuàng)建“學(xué)生”實(shí)例時(shí)使用的字段。
黑客為 Secret 表單字段指定的任何值都會在 DB 中更新。 下圖顯示 Fiddler 工具正在將 Secret 字段(值為“OverPost”)添加到已發(fā)布的表單值。
值“OverPost”已成功添加到所插入行的 Secret 屬性中。 應(yīng)用程序設(shè)計(jì)器絕不會在“創(chuàng)建”頁設(shè)置 Secret 屬性。
視圖模型通常包含應(yīng)用程序所用的模型中包括的屬性的子集。 應(yīng)用程序模型通常稱為域模型。 域模型通常包含 DB 中對應(yīng)實(shí)體所需的全部屬性。 視圖模型僅包含 UI 層(例如“創(chuàng)建”頁)所需的屬性。除視圖模型外,某些應(yīng)用使用綁定模型或輸入模型在“Razor 頁面”頁面模型類和瀏覽器之間傳遞數(shù)據(jù)。 請考慮以下 Student 視圖模型:
C#
using System;
namespace ContosoUniversity.Models
{
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
}
視圖模型還提供了一種防止過度發(fā)布的方法。 視圖模型僅包含要查看(顯示)或更新的屬性。
以下代碼使用 StudentVM 視圖模型創(chuàng)建新的學(xué)生:
C#
[BindProperty]
public StudentVM StudentVM { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var entry = _context.Add(new Student());
entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
SetValues 方法通過從另一個(gè) PropertyValues 對象讀取值來設(shè)置此對象的值。 SetValues 使用屬性名稱匹配。 視圖模型類型不需要與模型類型相關(guān),它只需要具有匹配的屬性。
使用 StudentVM 時(shí)需要更新 CreateVM.cshtml 才能使用 StudentVM 而非 Student。
在 Razor 頁面,PageModel 派生類就是視圖模型。
更新“編輯”頁的頁面模型。 突出顯示所作的主要更改:
C#
public class EditModel : PageModel
{
private readonly SchoolContext _context;
public EditModel(SchoolContext context)
{
_context = context;
}
[BindProperty]
public Student Student { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Student.FindAsync(id);
if (Student == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var studentToUpdate = await _context.Student.FindAsync(id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
}
代碼更改與“創(chuàng)建”頁類似,但有少數(shù)例外:
創(chuàng)建和編輯幾個(gè)學(xué)生實(shí)體。
DB 上下文會隨時(shí)跟蹤內(nèi)存中的實(shí)體是否已與其在 DB 中的對應(yīng)行進(jìn)行同步。 DB 上下文同步信息可決定調(diào)用 SaveChangesAsync 后的行為。 例如,將新實(shí)體傳遞到 AddAsync 方法時(shí),該實(shí)體的狀態(tài)設(shè)置為 Added。 調(diào)用 SaveChangesAsync 時(shí),DB 上下文會發(fā)出 SQL INSERT 命令。
實(shí)體可能處于以下狀態(tài)之一:
在桌面應(yīng)用中,通常會自動設(shè)置狀態(tài)更改。 讀取實(shí)體并執(zhí)行更改后,實(shí)體狀態(tài)自動更改為 Modified。 調(diào)用 SaveChanges 會生成僅更新已更改屬性的 SQL UPDATE 語句。
在 Web 應(yīng)用中,讀取實(shí)體并顯示數(shù)據(jù)的 DbContext 將在頁面呈現(xiàn)后進(jìn)行處理。 調(diào)用頁面 OnPostAsync 方法時(shí),將發(fā)出具有 DbContext 的新實(shí)例的 Web 請求。 如果在這個(gè)新的上下文中重新讀取實(shí)體,則會模擬桌面處理。
在此部分中,當(dāng)對 SaveChanges 的調(diào)用失敗時(shí),將添加用于實(shí)現(xiàn)自定義錯(cuò)誤消息的代碼。 添加字符串,使其包含可能的錯(cuò)誤消息:
C#
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 方法替換為以下代碼:
C#
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();
}
上述代碼包含可選參數(shù) saveChangesError。 saveChangesError 指示學(xué)生對象刪除失敗后是否調(diào)用該方法。 刪除操作可能由于暫時(shí)性網(wǎng)絡(luò)問題而失敗。 云端更可能出現(xiàn)暫時(shí)性網(wǎng)絡(luò)錯(cuò)誤。 通過 UI 調(diào)用“刪除”頁 OnGetAsync 時(shí),saveChangesError 為 false。 當(dāng) OnPostAsync 調(diào)用 OnGetAsync(由于刪除操作失敗)時(shí),saveChangesError 參數(shù)為 true。
將 OnPostAsync 替換為以下代碼:
C#
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 });
}
}
上述代碼檢索所選的實(shí)體,然后調(diào)用 Remove 方法,將實(shí)體的狀態(tài)設(shè)置為 Deleted。 調(diào)用 SaveChanges 時(shí)生成 SQL DELETE 命令。 如果 Remove 失?。?/p>
將以下突出顯示的錯(cuò)誤消息添加到“刪除”Razor 頁面。
CSHTML
@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>
測試“刪除”。
“學(xué)生/索引”或其他鏈接不起作用:
驗(yàn)證確認(rèn) Razor 頁面包含正確的 @page 指令。 例如,“學(xué)生/索引”Razor Pages 不得包含路由模板:
CSHTML
@page "{id:int}"
每個(gè) Razor 頁面均必須包含 @page 指令。
更多建議: