Contoso University Web 應用演示了如何使用 EF Core 和 Visual Studio 創(chuàng)建 Razor 頁面 Web 應用。若要了解系列教程,請參閱第一個教程。
本教程將介紹和自定義已搭建基架的 CRUD (創(chuàng)建、讀取、更新、刪除)代碼。
為最大程度降低復雜性并讓這些教程集中介紹 EF Core,將在頁面模型中使用 EF Core 代碼。 某些開發(fā)人員使用服務(wù)層或存儲庫模式在 UI(Razor 頁面)和數(shù)據(jù)訪問層之間創(chuàng)建抽象層。
本教程將檢查“學生”文件夾中的“創(chuàng)建”、“編輯”、“刪除”和“詳細信息”Razor Pages。
基架代碼將以下模式用于“創(chuàng)建”、“編輯”和“刪除”頁面:
“索引”和“詳細信息”頁面使用 HTTP GET 方法 OnGetAsync 獲取和顯示請求數(shù)據(jù)
生成的代碼使用 FirstOrDefaultAsync其推薦度通常高于 SingleOrDefaultAsync。
提取一個實體時,使用 FirstOrDefaultAsync 比使用 SingleOrDefaultAsync 更高效:
在大部分基架代碼中,FindAsync 可用于替代 FirstOrDefaultAsync。
FindAsync:
如果想要 Include 其他實體,則 FindAsync 將不再適用。 這意味著可能需要放棄 FindAsync 并隨著應用運行移動到查詢。
瀏覽到 Pages/Students 頁面。 “編輯”、“詳細信息”和“刪除”鏈接是在 Pages/Students/Index.cshtml 文件中由定位點標記幫助器生成的。
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>
運行應用并選擇“詳細信息”鏈接。 URL 的格式為 http://localhost:5000/Students/Details?id=2。 “學生 ID”通過查詢字符串 (?id=2) 進行傳遞。
更新“編輯”、“詳細信息”和“刪除”Razor 頁面以使用 "{id:int}" 路由模板。 將上述每個頁面的頁面指令從 @page 更改為 @page "{id:int}"。
如果對具有不包含整數(shù)路由值的“{id:int}”路由模板的頁面發(fā)起請求,則該請求將返回 HTTP 404(找不到)錯誤。 例如,http://localhost:5000/Students/Details 返回 404 錯誤。 若要使 ID 可選,請將 ? 追加到路由約束:
CSHTML
@page "{id:int?}"
運行應用,單擊“詳細信息”鏈接,并驗證確認 URL 正在將 ID 作為路由數(shù)據(jù) (http://localhost:5000/Students/Details/2) 進行傳遞。
不要將 @page 全局更改為 @page "{id:int}",執(zhí)行此操作會將鏈接拆分為“主頁”和“創(chuàng)建”頁。
“學生索引”頁的基架代碼不包括 Enrollments 屬性。 在本部分,Enrollments 集合的內(nèi)容顯示在“詳細信息”頁中。
Pages/Students/Details.cshtml.cs 的 OnGetAsync 方法使用 FirstOrDefaultAsync 方法檢索單個 Student 實體。 添加以下突出顯示的代碼:
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 導航屬性,并在每個注冊中加載 Enrollment.Course 導航屬性。 這些方法將在與數(shù)據(jù)讀取相關(guān)的教程中進行詳細介紹。
對于返回的實體未在當前上下文中更新的情況,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>
如果代碼縮進在粘貼代碼后出現(xiàn)錯誤,請按 CTRL-K-D 進行更正。
上面的代碼循環(huán)通過 Enrollments 導航屬性中的實體。 它將針對每個注冊顯示課程標題和成績。 課程標題從 Course 實體中檢索,該實體存儲在 Enrollments 實體的 Course 導航屬性中。
運行應用,選擇“學生”選項卡,然后單擊學生的“詳細信息”鏈接。 隨即顯示出所選學生的課程和成績列表。
將 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ā)布值的字段是一種最佳的安全做法,因為這能阻止過多發(fā)布。 例如,假設(shè) Student 實體包含此網(wǎ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; }
}
即使應用的創(chuàng)建/更新 Razor 頁面上沒有 Secret 字段,黑客仍可利用過多發(fā)布設(shè)置 Secret 值。 黑客也可使用 Fiddler 等工具或通過編寫某個 JavaScript 來發(fā)布 Secret 表單值。 原始代碼不會限制模型綁定器在創(chuàng)建“學生”實例時使用的字段。
黑客為 Secret 表單字段指定的任何值都會在 DB 中更新。 下圖顯示 Fiddler 工具正在將 Secret 字段(值為“OverPost”)添加到已發(fā)布的表單值。
值“OverPost”已成功添加到所插入行的 Secret 屬性中。 應用程序設(shè)計器絕不會在“創(chuàng)建”頁設(shè)置 Secret 屬性。
視圖模型通常包含應用程序所用的模型中包括的屬性的子集。 應用程序模型通常稱為域模型。 域模型通常包含 DB 中對應實體所需的全部屬性。 視圖模型僅包含 UI 層(例如“創(chuà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)建新的學生:
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 方法通過從另一個 PropertyValues 對象讀取值來設(shè)置此對象的值。 SetValues 使用屬性名稱匹配。 視圖模型類型不需要與模型類型相關(guān),它只需要具有匹配的屬性。
使用 StudentVM 時需要更新 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)建和編輯幾個學生實體。
DB 上下文會隨時跟蹤內(nèi)存中的實體是否已與其在 DB 中的對應行進行同步。 DB 上下文同步信息可決定調(diào)用 SaveChangesAsync 后的行為。 例如,將新實體傳遞到 AddAsync 方法時,該實體的狀態(tài)設(shè)置為 Added。 調(diào)用 SaveChangesAsync 時,DB 上下文會發(fā)出 SQL INSERT 命令。
實體可能處于以下狀態(tài)之一:
在桌面應用中,通常會自動設(shè)置狀態(tài)更改。 讀取實體并執(zhí)行更改后,實體狀態(tài)自動更改為 Modified。 調(diào)用 SaveChanges 會生成僅更新已更改屬性的 SQL UPDATE 語句。
在 Web 應用中,讀取實體并顯示數(shù)據(jù)的 DbContext 將在頁面呈現(xiàn)后進行處理。 調(diào)用頁面 OnPostAsync 方法時,將發(fā)出具有 DbContext 的新實例的 Web 請求。 如果在這個新的上下文中重新讀取實體,則會模擬桌面處理。
在此部分中,當對 SaveChanges 的調(diào)用失敗時,將添加用于實現(xiàn)自定義錯誤消息的代碼。 添加字符串,使其包含可能的錯誤消息:
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 指示學生對象刪除失敗后是否調(diào)用該方法。 刪除操作可能由于暫時性網(wǎng)絡(luò)問題而失敗。 云端更可能出現(xiàn)暫時性網(wǎng)絡(luò)錯誤。 通過 UI 調(diào)用“刪除”頁 OnGetAsync 時,saveChangesError 為 false。 當 OnPostAsync 調(diào)用 OnGetAsync(由于刪除操作失?。r,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 });
}
}
上述代碼檢索所選的實體,然后調(diào)用 Remove 方法,將實體的狀態(tài)設(shè)置為 Deleted。 調(diào)用 SaveChanges 時生成 SQL DELETE 命令。 如果 Remove 失?。?/p>
將以下突出顯示的錯誤消息添加到“刪除”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>
測試“刪除”。
“學生/索引”或其他鏈接不起作用:
驗證確認 Razor 頁面包含正確的 @page 指令。 例如,“學生/索引”Razor Pages 不得包含路由模板:
CSHTML
@page "{id:int}"
每個 Razor 頁面均必須包含 @page 指令。
更多建議: