ASP.NET Core 中的 Razor 頁(yè)面和 EF Core - CRUD

2019-04-17 08:57 更新

Contoso University Web 應(yīng)用演示了如何使用 EF Core 和 Visual Studio 創(chuàng)建 Razor 頁(yè)面 Web 應(yīng)用。若要了解系列教程,請(qǐng)參閱第一個(gè)教程。

本教程將介紹和自定義已搭建基架的 CRUD (創(chuàng)建、讀取、更新、刪除)代碼。

為最大程度降低復(fù)雜性并讓這些教程集中介紹 EF Core,將在頁(yè)面模型中使用 EF Core 代碼。 某些開(kāi)發(fā)人員使用服務(wù)層或存儲(chǔ)庫(kù)模式在 UI(Razor 頁(yè)面)和數(shù)據(jù)訪問(wèn)層之間創(chuàng)建抽象層。

本教程將檢查“學(xué)生”文件夾中的“創(chuàng)建”、“編輯”、“刪除”和“詳細(xì)信息”Razor Pages。

基架代碼將以下模式用于“創(chuàng)建”、“編輯”和“刪除”頁(yè)面:

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

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

SingleOrDefaultAsync 與FirstOrDefaultAsync

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

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

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

FindAsync

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

FindAsync:

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

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

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

瀏覽到 Pages/Students 頁(yè)面。 “編輯”、“詳細(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”通過(guò)查詢字符串 (?id=2) 進(jìn)行傳遞。

更新“編輯”、“詳細(xì)信息”和“刪除”Razor 頁(yè)面以使用 "{id:int}" 路由模板。 將上述每個(gè)頁(yè)面的頁(yè)面指令從 @page 更改為 @page "{id:int}"。

如果對(duì)具有不包含整數(shù)路由值的“{id:int}”路由模板的頁(yè)面發(fā)起請(qǐng)求,則該請(qǐng)求將返回 HTTP 404(找不到)錯(cuò)誤。 例如,http://localhost:5000/Students/Details 返回 404 錯(cuò)誤。 若要使 ID 可選,請(qǐng)將 ? 追加到路由約束:

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í)行此操作會(huì)將鏈接拆分為“主頁(yè)”和“創(chuàng)建”頁(yè)。

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

“學(xué)生索引”頁(yè)的基架代碼不包括 Enrollments 屬性。 在本部分,Enrollments 集合的內(nèi)容顯示在“詳細(xì)信息”頁(yè)中。

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è)注冊(cè)中加載 Enrollment.Course 導(dǎo)航屬性。 這些方法將在與數(shù)據(jù)讀取相關(guān)的教程中進(jìn)行詳細(xì)介紹。

對(duì)于返回的實(shí)體未在當(dāng)前上下文中更新的情況,AsNoTracking 方法將會(huì)提升性能。 AsNoTracking 將在本教程的后續(xù)部分中討論。

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

打開(kāi) Pages/Students/Details.cshtml。 添加以下突出顯示的代碼以顯示注冊(cè)列表:

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ò)誤,請(qǐng)按 CTRL-K-D 進(jìn)行更正。

上面的代碼循環(huán)通過(guò) Enrollments 導(dǎo)航屬性中的實(shí)體。 它將針對(duì)每個(gè)注冊(cè)顯示課程標(biāo)題和成績(jī)。 課程標(biāo)題從 Course 實(shí)體中檢索,該實(shí)體存儲(chǔ)在 Enrollments 實(shí)體的 Course 導(dǎo)航屬性中。

運(yùn)行應(yīng)用,選擇“學(xué)生”選項(xiàng)卡,然后單擊學(xué)生的“詳細(xì)信息”鏈接。 隨即顯示出所選學(xué)生的課程和成績(jī)列表。

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

將 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 對(duì)象。 TryUpdateModelAsync 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。

在上述示例中:

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

過(guò)多發(fā)布

使用 TryUpdateModel 更新具有已發(fā)布值的字段是一種最佳的安全做法,因?yàn)檫@能阻止過(guò)多發(fā)布。 例如,假設(shè) Student 實(shí)體包含此網(wǎng)頁(yè)不應(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 頁(yè)面上沒(méi)有 Secret 字段,黑客仍可利用過(guò)多發(fā)布設(shè)置 Secret 值。 黑客也可使用 Fiddler 等工具或通過(guò)編寫(xiě)某個(gè) JavaScript 來(lái)發(fā)布 Secret 表單值。 原始代碼不會(huì)限制模型綁定器在創(chuàng)建“學(xué)生”實(shí)例時(shí)使用的字段。

黑客為 Secret 表單字段指定的任何值都會(huì)在 DB 中更新。 下圖顯示 Fiddler 工具正在將 Secret 字段(值為“OverPost”)添加到已發(fā)布的表單值。

Fiddler 添加 Secret 字段

值“OverPost”已成功添加到所插入行的 Secret 屬性中。 應(yīng)用程序設(shè)計(jì)器絕不會(huì)在“創(chuàng)建”頁(yè)設(shè)置 Secret 屬性。

視圖模型

視圖模型通常包含應(yīng)用程序所用的模型中包括的屬性的子集。 應(yīng)用程序模型通常稱(chēng)為域模型。 域模型通常包含 DB 中對(duì)應(yīng)實(shí)體所需的全部屬性。 視圖模型僅包含 UI 層(例如“創(chuàng)建”頁(yè))所需的屬性。除視圖模型外,某些應(yīng)用使用綁定模型或輸入模型在“Razor 頁(yè)面”頁(yè)面模型類(lèi)和瀏覽器之間傳遞數(shù)據(jù)。 請(qǐng)考慮以下 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; }
    }
}

視圖模型還提供了一種防止過(guò)度發(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 方法通過(guò)從另一個(gè) PropertyValues 對(duì)象讀取值來(lái)設(shè)置此對(duì)象的值。 SetValues 使用屬性名稱(chēng)匹配。 視圖模型類(lèi)型不需要與模型類(lèi)型相關(guān),它只需要具有匹配的屬性。

使用 StudentVM 時(shí)需要更新 CreateVM.cshtml 才能使用 StudentVM 而非 Student。

在 Razor 頁(yè)面,PageModel 派生類(lèi)就是視圖模型。

更新“編輯”頁(yè)

更新“編輯”頁(yè)的頁(yè)面模型。 突出顯示所作的主要更改:

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)建”頁(yè)類(lèi)似,但有少數(shù)例外:

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

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

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

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

DB 上下文會(huì)隨時(shí)跟蹤內(nèi)存中的實(shí)體是否已與其在 DB 中的對(duì)應(yīng)行進(jìn)行同步。 DB 上下文同步信息可決定調(diào)用 SaveChangesAsync 后的行為。 例如,將新實(shí)體傳遞到 AddAsync 方法時(shí),該實(shí)體的狀態(tài)設(shè)置為 Added。 調(diào)用 SaveChangesAsync 時(shí),DB 上下文會(huì)發(fā)出 SQL INSERT 命令。

實(shí)體可能處于以下?tīng)顟B(tài)之一:

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

在桌面應(yīng)用中,通常會(huì)自動(dòng)設(shè)置狀態(tài)更改。 讀取實(shí)體并執(zhí)行更改后,實(shí)體狀態(tài)自動(dòng)更改為 Modified。 調(diào)用 SaveChanges 會(huì)生成僅更新已更改屬性的 SQL UPDATE 語(yǔ)句。

在 Web 應(yīng)用中,讀取實(shí)體并顯示數(shù)據(jù)的 DbContext 將在頁(yè)面呈現(xiàn)后進(jìn)行處理。 調(diào)用頁(yè)面 OnPostAsync 方法時(shí),將發(fā)出具有 DbContext 的新實(shí)例的 Web 請(qǐng)求。 如果在這個(gè)新的上下文中重新讀取實(shí)體,則會(huì)模擬桌面處理。

更新“刪除”頁(yè)

在此部分中,當(dāng)對(duì) 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é)生對(duì)象刪除失敗后是否調(diào)用該方法。 刪除操作可能由于暫時(shí)性網(wǎng)絡(luò)問(wèn)題而失敗。 云端更可能出現(xiàn)暫時(shí)性網(wǎng)絡(luò)錯(cuò)誤。 通過(guò) UI 調(diào)用“刪除”頁(yè) OnGetAsync 時(shí),saveChangesError 為 false。 當(dāng) OnPostAsync 調(diào)用 OnGetAsync(由于刪除操作失?。r(shí),saveChangesError 參數(shù)為 true。

“刪除”頁(yè) 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>

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

更新“刪除”Razor 頁(yè)面

將以下突出顯示的錯(cuò)誤消息添加到“刪除”Razor 頁(yè)面。

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>

測(cè)試“刪除”。

常見(jiàn)錯(cuò)誤

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

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

CSHTML

@page "{id:int}"

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)