ASP.NET Core 中的 Razor 頁面和 EF Core - CRUD

2019-04-17 08:57 更新

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)建”、“編輯”和“刪除”頁面:

  • 使用 HTTP GET 方法 OnGetAsync 獲取和顯示請求數(shù)據(jù)。
  • 使用 HTTP POST 方法 OnPostAsync 將更改保存到數(shù)據(jù)。

“索引”和“詳細(xì)信息”頁面使用 HTTP GET 方法 OnGetAsync 獲取和顯示請求數(shù)據(jù)

SingleOrDefaultAsync 與FirstOrDefaultAsync

生成的代碼使用 FirstOrDefaultAsync其推薦度通常高于 SingleOrDefaultAsync

提取一個(gè)實(shí)體時(shí),使用 FirstOrDefaultAsync 比使用 SingleOrDefaultAsync 更高效:

  • 代碼需要驗(yàn)證查詢僅返回一個(gè)實(shí)體時(shí)除外。
  • SingleOrDefaultAsync 會提取更多數(shù)據(jù)并執(zhí)行不必要的工作。
  • 如果有多個(gè)實(shí)體符合篩選部分,SingleOrDefaultAsync 將引發(fā)異常。
  • 如果有多個(gè)實(shí)體符合篩選部分,F(xiàn)irstOrDefaultAsync 不引發(fā)異常。

FindAsync

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

FindAsync:

  • 查找具有主鍵 (PK) 的實(shí)體。 如果具有 PK 的實(shí)體正在由上下文跟蹤,會返回該實(shí)體且不向 DB 發(fā)出請求。
  • 既簡單又簡潔。
  • 經(jīng)過優(yōu)化后可查找單個(gè)實(shí)體。
  • 在某些情況下可以提供性能優(yōu)勢,但很少發(fā)生在典型的 Web 應(yīng)用中。
  • 以隱式方式使用 FirstAsync 而不是 SingleAsync。

如果想要 Include 其他實(shí)體,則 FindAsync 將不再適用。 這意味著可能需要放棄 FindAsync 并隨著應(yīng)用運(yùn)行移動到查詢。

自定義“詳細(xì)信息”頁

瀏覽到 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)建”頁。

添加相關(guān)數(shù)據(jù)

“學(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ù)部分中討論。

在“詳細(xì)信息”頁中顯示相關(guān)注冊

打開 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é)生的課程和成績列表。

更新“創(chuàng)建”頁

將 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

檢查 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)。

在上述示例中:

  • 第二個(gè)自變量 ("student", // Prefix) 是用于查找值的前綴。 該自變量不區(qū)分大小寫。
  • 已發(fā)布的表單值通過模型綁定轉(zhuǎn)換為 Student 模型中的類型。

過多發(fā)布

使用 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ā)布的表單值。

Fiddler 添加 Secret 字段

值“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ù)例外:

  • OnPostAsync 具有可選的 id 參數(shù)。
  • 當(dāng)前學(xué)生是從 DB 提取的,而非通過創(chuàng)建空學(xué)生獲得。
  • 已將 FirstOrDefaultAsync 替換為 FindAsync。 從主鍵中選擇實(shí)體時(shí),使用 FindAsync 是一個(gè)不錯(cuò)的選擇。 請參閱 FindAsync 了解詳細(xì)信息。

測試“編輯”和“創(chuàng)建”頁

創(chuàng)建和編輯幾個(gè)學(xué)生實(shí)體。

實(shí)體狀態(tài)

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)之一:

  • Added:DB 中尚不存在實(shí)體。 SaveChanges 方法發(fā)出 INSERT 語句。
  • Unchanged:無需保存對該實(shí)體所做的任何更改。 從 DB 中讀取實(shí)體時(shí),該實(shí)體將具有此狀態(tài)。
  • Modified:已修改實(shí)體的部分或全部屬性值。 SaveChanges 方法發(fā)出 UPDATE 語句。
  • Deleted:已標(biāo)記該實(shí)體進(jìn)行刪除。 SaveChanges 方法發(fā)出 DELETE 語句。
  • Detached:DB 上下文未跟蹤該實(shí)體。

在桌面應(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 方法

將 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>

  • 會捕獲 DB 異常。
  • 通過 saveChangesError=true 調(diào)用“刪除”頁 OnGetAsync 方法。

更新“刪除”Razor 頁面

將以下突出顯示的錯(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>

測試“刪除”。

常見錯(cuò)誤

“學(xué)生/索引”或其他鏈接不起作用:

驗(yàn)證確認(rèn) Razor 頁面包含正確的 @page 指令。 例如,“學(xué)生/索引”Razor Pages 不得包含路由模板:

CSHTML

@page "{id:int}"

每個(gè) Razor 頁面均必須包含 @page 指令。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號