第 2 部分,在 ASP.NET Core 中使用 EF Core 的 Razor 頁面 - CRUD

作者:Tom DykstraJeremy LiknessJon P Smith

Contoso 大學 Web 應用程式將示範如何使用 EF Core 和 Visual Studio 來建立 Razor Pages Web 應用程式。 如需教學課程系列的資訊,請參閱第一個教學課程

如果您遇到無法解決的問題,請下載已完成的應用程式,並遵循本教學課程以將程式碼與您所建立的內容進行比較。

在本教學課程中,將會檢閱並自訂 Scaffold CRUD (建立、讀取、更新、刪除)。

沒有任何存放庫

有些開發人員會使用服務層或存放庫模式來建立介於 UI (Razor Pages) 和資料存取層之間的抽象層。 本教學課程不會這麼做。 為降低複雜性並將教學課程聚焦於 EF Core,EF Core 程式碼會直接新增至頁面模型類別中。

更新 [詳細資料] 頁面

Students 頁面的 Scaffold 程式碼不包含註冊資料。 在本節中,註冊會新增至 Details 頁面。

讀取註冊

若要在頁面上顯示學生的註冊資料,則必須讀取註冊資料。 Pages/Students/Details.cshtml.cs 中自動產生的程式碼只會讀取 Student 資料,但不含 Enrollment 資料:

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

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

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

OnGetAsync 方法取代為下列程式碼,以讀取所選學生的註冊資料。 所做的變更已醒目提示。

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

IncludeThenInclude 方法會導致內容載入 Student.Enrollments 導覽屬性,並在每個註冊中載入 Enrollment.Course 導覽屬性。 您可在讀取相關資料教學課程中詳細檢視這些方法。

AsNoTracking 方法可提高傳回實體未在目前內容中更新的情況下的效能。 AsNoTracking 稍後在本教學課程中將會討論。

顯示註冊

Pages/Students/Details.cshtml 中的程式碼取代為下列程式碼以顯示註冊清單。 所做的變更已醒目提示。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <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>

上述程式碼會以迴圈逐一巡覽 Enrollments 導覽屬性中的實體。 針對每個註冊,會顯示課程標題及成績。 課程標題會從儲存在 Enrollments 實體的 Course 導覽屬性中的 Course 實體中擷取。

執行應用程式,選取 [Students] 索引標籤,然後按一下學生的 [詳細資料] 連結。 將會顯示所選取學生的課程及成績清單。

讀取單一實體的方式

產生的程式碼會使用 FirstOrDefaultAsync 來讀取單一實體。 如果找不到任何內容,則此方法會傳回 Null;否則會傳回符合查詢篩選準則的第一個資料列。 FirstOrDefaultAsync 通常是比下列替代選項更好的選擇:

  • SingleOrDefaultAsync - 如果有超過一個滿足查詢篩選準則的實體,就會擲回例外狀況。 為了判斷查詢是否可能傳回超過一個資料列,SingleOrDefaultAsync 會嘗試提取多個資料列。 如果查詢只能傳回單一實體 (如同在搜尋唯一索引鍵時一樣),則此額外工作是不必要的。
  • FindAsync - 尋找含有主索引鍵 (PK) 的實體。 如果內容正在追蹤具有主索引鍵的實體,則會在沒有傳送要求至資料庫的情況下傳回。 此方法已經過最佳化以便查閱單一實體,但是您無法使用 FindAsync 呼叫 Include。 因此如果需要相關資料,FirstOrDefaultAsync 是較好的選擇。

路由資料與查詢字串

[詳細資料] 頁面的 URL 是 https://localhost:<port>/Students/Details?id=1。 實體的主要索引鍵值位於查詢字串中。 某些開發人員偏好在路由資料中傳遞索引鍵值:https://localhost:<port>/Students/Details/1。 如需詳細資訊,請參閱更新產生的程式碼

更新 [建立] 頁面

[建立] 頁面的 scaffold OnPostAsync 程式碼容易受到大量指派攻擊。 以下列程式碼取代 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法。

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

上述程式碼會建立 Student 物件,然後使用張貼的表單欄位來更新 Student 物件屬性。 TryUpdateModelAsync 方法:

  • 使用 PageModelPageContext 屬性所發佈的表單值。
  • 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • 尋找具有 "student" 前置詞的表單欄位。 例如: Student.FirstMidName 。 不區分大小寫。
  • 使用模型繫結系統,將字串中表單值轉換成 Student 模型中的型別。 例如,將 EnrollmentDate 轉換成 DateTime

執行應用程式,並建立 Student 實體來測試 [建立] 頁面。

大量指派 (overposting)

使用 TryUpdateModel 來更新具有已張貼值欄位是最安全的做法,因為它會防止大量指派 (overposting)。 例如,假設 Student 實體包含了一個此網頁不應更新或新增的 Secret 屬性:

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; }
}

即使應用程式在建立或更新 Razor 頁面上沒有 Secret 欄位,駭客仍然可以藉由大量指派來設定 Secret 值。 駭客仍可能使用 Fiddler 等工具,或是撰寫 JavaScript,來張貼 Secret 表單值。 原始程式碼並不會限制模型繫結器在建立 Student 執行個體時所使用的欄位。

無論駭客在 Secret 表單欄位中指定了什麼值,該值都會更新到資料庫中。 下圖顯示了 Fiddler 工具將 Secret 欄位 (其值為 "OverPost") 新增到已發佈的表單值中。

Fiddler adding Secret field

"OverPost" 值已成功新增至插入資料列的 Secret 屬性。 即使應用程式設計師從未打算在 [建立] 頁面設定 Secret 屬性,還是會發生此情況。

檢視模型

檢視模型提供防止大量指派 (overposting) 的替代方法。

應用程式模型通常稱為網域模型。 網域模型通常會包含資料庫中對應實體所需要的所有屬性。 檢視模型只包含 UI 頁面所需要的屬性 (例如 [建立] 頁面)。

除了檢視模型之外,某些應用程式會使用繫結模型或輸入模型,在 Razor Pages 頁面模型類別和瀏覽器之間傳遞資料。

請看看下列 StudentVM 檢視模型:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

下列程式碼會使用 StudentVM 檢視模型來建立一名新學生:

[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 物件中讀取值。 SetValues 使用屬性名稱比對。 檢視模型類型:

  • 不需要與模型類型相關。
  • 必須有相符的屬性。

使用 StudentVM 需要 [建立] 頁面使用 StudentVM,而不是 Student

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

更新 [編輯] 頁面

Pages/Students/Edit.cshtml.cs 中,以下列程式碼取代 OnGetAsyncOnPostAsync 方法。

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

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

程式碼的變更與 [建立] 頁面相似,但有一些例外狀況:

  • FirstOrDefaultAsync 已取代為 FindAsync。 如果您不需要包含相關資料,則 FindAsync 效率較高。
  • OnPostAsync 具有 id 參數。
  • 會從資料庫中擷取出目前的學生,而不會建立一個空白的學生資料。

執行應用程式,並藉由建立和編輯學生來進行測試。

實體狀態

無論記憶體中實體是否與資料庫中相對應資料列同步,資料庫內容都會持續追蹤。 呼叫 SaveChangesAsync 時,此追蹤資訊會決定該怎麼做。 例如,當傳遞一個新的實體到 AddAsync 方法時,該實體的狀態便會設定為 Added。 呼叫 SaveChangesAsync 時,資料庫內容會發出 SQL INSERT 命令。

實體可為下列狀態中的其中一個:

  • Added:實體尚未存在於資料庫中。 SaveChanges 方法會發出 INSERT 陳述式。

  • Unchanged:此實體沒有需要儲存的變更。 從資料庫讀取時,此實體將會有這個狀態。

  • Modified:實體中一部分或全部的屬性值已經過修改。 SaveChanges 方法會發出 UPDATE 陳述式。

  • Deleted:實體已遭標示刪除。 SaveChanges 方法會發出 DELETE 陳述式。

  • Detached:實體未獲得資料庫內容追蹤。

在桌面應用程式中,狀態變更通常會自動進行設定。 實體已讀取、已進行變更,且實體狀態會自動變更為 Modified。 呼叫 SaveChanges 會產生 SQL UPDATE 陳述式,此陳述式只會更新變更過的屬性。

在 Web 應用程式中,讀取實體並顯示其資料以供編輯之用的 DbContext 會在頁面轉譯之後遭到處置。 當呼叫頁面的 OnPostAsync 方法時,會發出新的 Web 要求,並且會擁有一個新的 DbContext 執行個體。 在新的內容中重新讀取實體時,會模擬桌面處理流程。

更新 [刪除] 頁面

在本節中,當呼叫 SaveChanges 失敗時,會實作一個自訂錯誤訊息。

以下列程式碼取代 Pages/Students/Delete.cshtml.cs 中的程式碼:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

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

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

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

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

上述 程式碼:

  • 新增記錄
  • 將選擇性參數 saveChangesError 新增至 OnGetAsync 方法簽章。 saveChangesError 會顯示出此方法是否已在刪除學生物件失敗後呼叫。

刪除作業可能會因為暫時性的網路問題而失敗。 資料庫位於雲端時,較可能發生暫時性的網路錯誤。 從 UI 中呼叫 [刪除] 頁面 OnGetAsync 時,saveChangesError 參數為 false。 當 OnPostAsync 因刪除作業失敗而呼叫 OnGetAsync 時,saveChangesError 參數為 true

OnPostAsync 方法會擷取選取的實體,然後呼叫 Remove 方法來將實體的狀態設定為 Deleted。 當呼叫 SaveChanges 時,便會產生 SQL DELETE 命令。 如果 Remove 失敗:

  • 攔截到資料庫例外狀況。
  • [刪除] 頁面的 OnGetAsync 方法會以 saveChangesError=true 呼叫。

將錯誤訊息新增至 Pages/Students/Delete.cshtml

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

執行應用程式並刪除學生以測試 [刪除] 頁面。

下一步

在本教學課程中,將會檢閱並自訂 Scaffold CRUD (建立、讀取、更新、刪除)。

沒有任何存放庫

有些開發人員會使用服務層或存放庫模式來建立介於 UI (Razor Pages) 和資料存取層之間的抽象層。 本教學課程不會這麼做。 為降低複雜性並將教學課程聚焦於 EF Core,EF Core 程式碼會直接新增至頁面模型類別中。

更新 [詳細資料] 頁面

Students 頁面的 Scaffold 程式碼不包含註冊資料。 在本節中,註冊會新增至 Details 頁面。

讀取註冊

若要在頁面上顯示學生的註冊資料,則必須讀取註冊資料。 Pages/Students/Details.cshtml.cs 中自動產生的程式碼只會讀取 Student 資料,但不含 Enrollment 資料:

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

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

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

OnGetAsync 方法取代為下列程式碼,以讀取所選學生的註冊資料。 所做的變更已醒目提示。

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

IncludeThenInclude 方法會導致內容載入 Student.Enrollments 導覽屬性,並在每個註冊中載入 Enrollment.Course 導覽屬性。 您可在讀取相關資料教學課程中詳細檢視這些方法。

AsNoTracking 方法可提高傳回實體未在目前內容中更新的情況下的效能。 AsNoTracking 稍後在本教學課程中將會討論。

顯示註冊

Pages/Students/Details.cshtml 中的程式碼取代為下列程式碼以顯示註冊清單。 所做的變更已醒目提示。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <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>

上述程式碼會以迴圈逐一巡覽 Enrollments 導覽屬性中的實體。 針對每個註冊,會顯示課程標題及成績。 課程標題會從儲存在 Enrollments 實體的 Course 導覽屬性中的 Course 實體中擷取。

執行應用程式,選取 [Students] 索引標籤,然後按一下學生的 [詳細資料] 連結。 將會顯示所選取學生的課程及成績清單。

讀取單一實體的方式

產生的程式碼會使用 FirstOrDefaultAsync 來讀取單一實體。 如果找不到任何內容,則此方法會傳回 Null;否則會傳回符合查詢篩選準則的第一個資料列。 FirstOrDefaultAsync 通常是比下列替代選項更好的選擇:

  • SingleOrDefaultAsync - 如果有超過一個滿足查詢篩選準則的實體,就會擲回例外狀況。 為了判斷查詢是否可能傳回超過一個資料列,SingleOrDefaultAsync 會嘗試提取多個資料列。 如果查詢只能傳回單一實體 (如同在搜尋唯一索引鍵時一樣),則此額外工作是不必要的。
  • FindAsync - 尋找含有主索引鍵 (PK) 的實體。 如果內容正在追蹤具有主索引鍵的實體,則會在沒有傳送要求至資料庫的情況下傳回。 此方法已經過最佳化以便查閱單一實體,但是您無法使用 FindAsync 呼叫 Include。 因此如果需要相關資料,FirstOrDefaultAsync 是較好的選擇。

路由資料與查詢字串

[詳細資料] 頁面的 URL 是 https://localhost:<port>/Students/Details?id=1。 實體的主要索引鍵值位於查詢字串中。 某些開發人員偏好在路由資料中傳遞索引鍵值:https://localhost:<port>/Students/Details/1。 如需詳細資訊,請參閱更新產生的程式碼

更新 [建立] 頁面

[建立] 頁面的 scaffold OnPostAsync 程式碼容易受到大量指派攻擊。 以下列程式碼取代 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法。

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

上述程式碼會建立 Student 物件,然後使用張貼的表單欄位來更新 Student 物件屬性。 TryUpdateModelAsync 方法:

  • 使用 PageModelPageContext 屬性所發佈的表單值。
  • 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • 尋找具有 "student" 前置詞的表單欄位。 例如: Student.FirstMidName 。 不區分大小寫。
  • 使用模型繫結系統,將字串中表單值轉換成 Student 模型中的型別。 例如,將 EnrollmentDate 轉換成 DateTime

執行應用程式,並建立 Student 實體來測試 [建立] 頁面。

大量指派 (overposting)

使用 TryUpdateModel 來更新具有已張貼值欄位是最安全的做法,因為它會防止大量指派 (overposting)。 例如,假設 Student 實體包含了一個此網頁不應更新或新增的 Secret 屬性:

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; }
}

即使應用程式在建立或更新 Razor 頁面上沒有 Secret 欄位,駭客仍然可以藉由大量指派來設定 Secret 值。 駭客仍可能使用 Fiddler 等工具,或是撰寫 JavaScript,來張貼 Secret 表單值。 原始程式碼並不會限制模型繫結器在建立 Student 執行個體時所使用的欄位。

無論駭客在 Secret 表單欄位中指定了什麼值,該值都會更新到資料庫中。 下圖顯示了 Fiddler 工具將 Secret 欄位 (其值為 "OverPost") 新增到已發佈的表單值中。

Fiddler adding Secret field

"OverPost" 值已成功新增至插入資料列的 Secret 屬性。 即使應用程式設計師從未打算在 [建立] 頁面設定 Secret 屬性,還是會發生此情況。

檢視模型

檢視模型提供防止大量指派 (overposting) 的替代方法。

應用程式模型通常稱為網域模型。 網域模型通常會包含資料庫中對應實體所需要的所有屬性。 檢視模型只包含 UI 頁面所需要的屬性 (例如 [建立] 頁面)。

除了檢視模型之外,某些應用程式會使用繫結模型或輸入模型,在 Razor Pages 頁面模型類別和瀏覽器之間傳遞資料。

請看看下列 StudentVM 檢視模型:

public class StudentVM
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
}

下列程式碼會使用 StudentVM 檢視模型來建立一名新學生:

[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 物件中讀取值。 SetValues 使用屬性名稱比對。 檢視模型類型:

  • 不需要與模型類型相關。
  • 必須有相符的屬性。

使用 StudentVM 需要 [建立] 頁面使用 StudentVM,而不是 Student

@page
@model CreateVMModel

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

<h1>Create</h1>

<h4>Student</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="StudentVM.LastName" class="control-label"></label>
                <input asp-for="StudentVM.LastName" class="form-control" />
                <span asp-validation-for="StudentVM.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.FirstMidName" class="control-label"></label>
                <input asp-for="StudentVM.FirstMidName" class="form-control" />
                <span asp-validation-for="StudentVM.FirstMidName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StudentVM.EnrollmentDate" class="control-label"></label>
                <input asp-for="StudentVM.EnrollmentDate" class="form-control" />
                <span asp-validation-for="StudentVM.EnrollmentDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

更新 [編輯] 頁面

Pages/Students/Edit.cshtml.cs 中,以下列程式碼取代 OnGetAsyncOnPostAsync 方法。

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

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

程式碼的變更與 [建立] 頁面相似,但有一些例外狀況:

  • FirstOrDefaultAsync 已取代為 FindAsync。 如果您不需要包含相關資料,則 FindAsync 效率較高。
  • OnPostAsync 具有 id 參數。
  • 會從資料庫中擷取出目前的學生,而不會建立一個空白的學生資料。

執行應用程式,並藉由建立和編輯學生來進行測試。

實體狀態

無論記憶體中實體是否與資料庫中相對應資料列同步,資料庫內容都會持續追蹤。 呼叫 SaveChangesAsync 時,此追蹤資訊會決定該怎麼做。 例如,當傳遞一個新的實體到 AddAsync 方法時,該實體的狀態便會設定為 Added。 呼叫 SaveChangesAsync 時,資料庫內容會發出 SQL INSERT 命令。

實體可為下列狀態中的其中一個:

  • Added:實體尚未存在於資料庫中。 SaveChanges 方法會發出 INSERT 陳述式。

  • Unchanged:此實體沒有需要儲存的變更。 從資料庫讀取時,此實體將會有這個狀態。

  • Modified:實體中一部分或全部的屬性值已經過修改。 SaveChanges 方法會發出 UPDATE 陳述式。

  • Deleted:實體已遭標示刪除。 SaveChanges 方法會發出 DELETE 陳述式。

  • Detached:實體未獲得資料庫內容追蹤。

在桌面應用程式中,狀態變更通常會自動進行設定。 實體已讀取、已進行變更,且實體狀態會自動變更為 Modified。 呼叫 SaveChanges 會產生 SQL UPDATE 陳述式,此陳述式只會更新變更過的屬性。

在 Web 應用程式中,讀取實體並顯示其資料以供編輯之用的 DbContext 會在頁面轉譯之後遭到處置。 當呼叫頁面的 OnPostAsync 方法時,會發出新的 Web 要求,並且會擁有一個新的 DbContext 執行個體。 在新的內容中重新讀取實體時,會模擬桌面處理流程。

更新 [刪除] 頁面

在本節中,當呼叫 SaveChanges 失敗時,會實作一個自訂錯誤訊息。

以下列程式碼取代 Pages/Students/Delete.cshtml.cs 中的程式碼:

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;
        private readonly ILogger<DeleteModel> _logger;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context,
                           ILogger<DeleteModel> logger)
        {
            _context = context;
            _logger = logger;
        }

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

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

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

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

            if (saveChangesError.GetValueOrDefault())
            {
                ErrorMessage = String.Format("Delete {ID} failed. Try again", id);
            }

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, ErrorMessage);

                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

上述 程式碼:

  • 新增記錄
  • 將選擇性參數 saveChangesError 新增至 OnGetAsync 方法簽章。 saveChangesError 會顯示出此方法是否已在刪除學生物件失敗後呼叫。

刪除作業可能會因為暫時性的網路問題而失敗。 資料庫位於雲端時,較可能發生暫時性的網路錯誤。 從 UI 中呼叫 [刪除] 頁面 OnGetAsync 時,saveChangesError 參數為 false。 當 OnPostAsync 因刪除作業失敗而呼叫 OnGetAsync 時,saveChangesError 參數為 true

OnPostAsync 方法會擷取選取的實體,然後呼叫 Remove 方法來將實體的狀態設定為 Deleted。 當呼叫 SaveChanges 時,便會產生 SQL DELETE 命令。 如果 Remove 失敗:

  • 攔截到資料庫例外狀況。
  • [刪除] 頁面的 OnGetAsync 方法會以 saveChangesError=true 呼叫。

將錯誤訊息新增至 Pages/Students/Delete.cshtml

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

執行應用程式並刪除學生以測試 [刪除] 頁面。

下一步

在本教學課程中,將會檢閱並自訂 Scaffold CRUD (建立、讀取、更新、刪除)。

沒有任何存放庫

有些開發人員會使用服務層或存放庫模式來建立介於 UI (Razor Pages) 和資料存取層之間的抽象層。 本教學課程不會這麼做。 為降低複雜性並將教學課程聚焦於 EF Core,EF Core 程式碼會直接新增至頁面模型類別中。

更新 [詳細資料] 頁面

Students 頁面的 Scaffold 程式碼不包含註冊資料。 在本節中,註冊會新增至 [詳細資料] 頁面。

讀取註冊

若要在頁面上顯示學生的註冊資料,則必須讀取註冊資料。 Pages/Students/Details.cshtml.cs 中自動產生的程式碼只會讀取 Student 資料,但不含 Enrollment 資料:

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

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

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

OnGetAsync 方法取代為下列程式碼,以讀取所選學生的註冊資料。 所做的變更已醒目提示。

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

    Student = await _context.Students
        .Include(s => s.Enrollments)
        .ThenInclude(e => e.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);

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

IncludeThenInclude 方法會導致內容載入 Student.Enrollments 導覽屬性,並在每個註冊中載入 Enrollment.Course 導覽屬性。 您可在讀取相關資料教學課程中詳細檢視這些方法。

AsNoTracking 方法可提高傳回實體未在目前內容中更新的情況下的效能。 AsNoTracking 稍後在本教學課程中將會討論。

顯示註冊

Pages/Students/Details.cshtml 中的程式碼取代為下列程式碼以顯示註冊清單。 所做的變更已醒目提示。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <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>

上述程式碼會以迴圈逐一巡覽 Enrollments 導覽屬性中的實體。 針對每個註冊,會顯示課程標題及成績。 課程標題會從儲存於 Enrollments 實體之 Course 導覽屬性中的課程 (Course) 實體擷取。

執行應用程式,選取 [Students] 索引標籤,然後按一下學生的 [詳細資料] 連結。 將會顯示所選取學生的課程及成績清單。

讀取單一實體的方式

產生的程式碼會使用 FirstOrDefaultAsync 來讀取單一實體。 如果找不到任何內容,則此方法會傳回 Null;否則會傳回符合查詢篩選準則的第一個資料列。 FirstOrDefaultAsync 通常是比下列替代選項更好的選擇:

  • SingleOrDefaultAsync - 如果有超過一個滿足查詢篩選準則的實體,就會擲回例外狀況。 為了判斷查詢是否可能傳回超過一個資料列,SingleOrDefaultAsync 會嘗試提取多個資料列。 如果查詢只能傳回單一實體 (如同在搜尋唯一索引鍵時一樣),則此額外工作是不必要的。
  • FindAsync - 尋找含有主索引鍵 (PK) 的實體。 如果內容正在追蹤具有主索引鍵的實體,則會在沒有傳送要求至資料庫的情況下傳回。 此方法已經過最佳化以便查閱單一實體,但是您無法使用 FindAsync 呼叫 Include。 因此如果需要相關資料,FirstOrDefaultAsync 是較好的選擇。

路由資料與查詢字串

[詳細資料] 頁面的 URL 是 https://localhost:<port>/Students/Details?id=1。 實體的主要索引鍵值位於查詢字串中。 某些開發人員偏好在路由資料中傳遞索引鍵值:https://localhost:<port>/Students/Details/1。 如需詳細資訊,請參閱更新產生的程式碼

更新 [建立] 頁面

[建立] 頁面的 scaffold OnPostAsync 程式碼容易受到大量指派攻擊。 以下列程式碼取代 Pages/Students/Create.cshtml.cs 中的 OnPostAsync 方法。

public async Task<IActionResult> OnPostAsync()
{
    var emptyStudent = new Student();

    if (await TryUpdateModelAsync<Student>(
        emptyStudent,
        "student",   // Prefix for form value.
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        _context.Students.Add(emptyStudent);
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

TryUpdateModelAsync

上述程式碼會建立 Student 物件,然後使用張貼的表單欄位來更新 Student 物件屬性。 TryUpdateModelAsync 方法:

  • 使用 PageModelPageContext 屬性所發佈的表單值。
  • 僅更新列出的屬性 (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • 尋找具有 "student" 前置詞的表單欄位。 例如: Student.FirstMidName 。 不區分大小寫。
  • 使用模型繫結系統,將字串中表單值轉換成 Student 模型中的型別。 例如,EnrollmentDate 必須轉換成 DateTime。

執行應用程式,並建立 Student 實體來測試 [建立] 頁面。

大量指派 (overposting)

使用 TryUpdateModel 來更新具有已張貼值欄位是最安全的做法,因為它會防止大量指派 (overposting)。 例如,假設 Student 實體包含了一個此網頁不應更新或新增的 Secret 屬性:

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; }
}

即使應用程式在建立或更新 Razor 頁面上沒有 Secret 欄位,駭客仍然可以藉由大量指派來設定 Secret 值。 駭客仍可能使用 Fiddler 等工具,或是撰寫 JavaScript,來張貼 Secret 表單值。 原始程式碼並不會限制模型繫結器在建立 Student 執行個體時所使用的欄位。

無論駭客在 Secret 表單欄位中指定了什麼值,該值都會更新到資料庫中。 下列影響顯示了 Fiddler 工具將 Secret 欄位 (其值為 "OverPost") 新增到表單的值中。

Fiddler adding Secret field

"OverPost" 值已成功新增至插入資料列的 Secret 屬性。 即使應用程式設計師從未打算在 [建立] 頁面設定 Secret 屬性,還是會發生此情況。

檢視模型

檢視模型提供防止大量指派 (overposting) 的替代方法。

應用程式模型通常稱為網域模型。 網域模型通常會包含資料庫中對應實體所需要的所有屬性。 檢視模型只包含 UI 所需要的屬性 (例如 [建立] 頁面)。

除了檢視模型之外,某些應用程式會使用繫結模型或輸入模型,在 Razor Pages 頁面模型類別和瀏覽器之間傳遞資料。

請看看下列 Student 檢視模型:

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; }
    }
}

下列程式碼會使用 StudentVM 檢視模型來建立一名新學生:

[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 物件中讀取值。 SetValues 使用屬性名稱比對。 檢視模型類型不需要與模型類型相關,只需要有符合的屬性。

使用 StudentVM 需要 Create.cshtml 更新為使用 StudentVM 而不是 Student

更新 [編輯] 頁面

Pages/Students/Edit.cshtml.cs 中,以下列程式碼取代 OnGetAsyncOnPostAsync 方法。

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

    Student = await _context.Students.FindAsync(id);

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

public async Task<IActionResult> OnPostAsync(int id)
{
    var studentToUpdate = await _context.Students.FindAsync(id);

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

    if (await TryUpdateModelAsync<Student>(
        studentToUpdate,
        "student",
        s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
    {
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    return Page();
}

程式碼的變更與 [建立] 頁面相似,但有一些例外狀況:

  • FirstOrDefaultAsync 已取代為 FindAsync。 當不需要包含的相關資料時,FindAsync 會更有效率。
  • OnPostAsync 具有 id 參數。
  • 會從資料庫中擷取出目前的學生,而不會建立一個空白的學生資料。

執行應用程式,並藉由建立和編輯學生來進行測試。

實體狀態

無論記憶體中實體是否與資料庫中相對應資料列同步,資料庫內容都會持續追蹤。 呼叫 SaveChangesAsync 時,此追蹤資訊會決定該怎麼做。 例如,當傳遞一個新的實體到 AddAsync 方法時,該實體的狀態便會設定為 Added。 呼叫 SaveChangesAsync 時,資料庫內容會發出 SQL INSERT 命令。

實體可為下列狀態中的其中一個:

  • Added:實體尚未存在於資料庫中。 SaveChanges 方法會發出 INSERT 陳述式。

  • Unchanged:此實體沒有需要儲存的變更。 從資料庫讀取時,此實體將會有這個狀態。

  • Modified:實體中一部分或全部的屬性值已經過修改。 SaveChanges 方法會發出 UPDATE 陳述式。

  • Deleted:實體已遭標示刪除。 SaveChanges 方法會發出 DELETE 陳述式。

  • Detached:實體未獲得資料庫內容追蹤。

在桌面應用程式中,狀態變更通常會自動進行設定。 實體已讀取、已進行變更,且實體狀態會自動變更為 Modified。 呼叫 SaveChanges 會產生 SQL UPDATE 陳述式,此陳述式只會更新變更過的屬性。

在 Web 應用程式中,讀取實體並顯示其資料以供編輯之用的 DbContext 會在頁面轉譯之後遭到處置。 當呼叫頁面的 OnPostAsync 方法時,會發出新的 Web 要求,並且會擁有一個新的 DbContext 執行個體。 在新的內容中重新讀取實體時,會模擬桌面處理流程。

更新 [刪除] 頁面

在本節中,您將實作一個呼叫至 SaveChanges 失敗時的自訂錯誤訊息。

Pages/Students/Delete.cshtml.cs 中的程式碼取代為下列程式碼。 變更會醒目提示 (除了清除 using 陳述式之外)。

using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class DeleteModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

        public DeleteModel(ContosoUniversity.Data.SchoolContext context)
        {
            _context = context;
        }

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

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

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

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

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

            return Page();
        }

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

            var student = await _context.Students.FindAsync(id);

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

            try
            {
                _context.Students.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 });
            }
        }
    }
}

上述程式碼會將選擇性參數 saveChangesError 新增至 OnGetAsync 方法簽章。 saveChangesError 會顯示出此方法是否已在刪除學生物件失敗後呼叫。 刪除作業可能會因為暫時性的網路問題而失敗。 資料庫位於雲端時,較可能發生暫時性的網路錯誤。 從 UI 中呼叫 [刪除] 頁面 OnGetAsync 時,saveChangesError 參數為 false。 當 OnPostAsync 呼叫 OnGetAsync 時 (因刪除作業失敗),則 saveChangesError 參數為 true。

OnPostAsync 方法會擷取選取的實體,然後呼叫 Remove 方法來將實體的狀態設定為 Deleted。 當呼叫 SaveChanges 時,便會產生 SQL DELETE 命令。 如果 Remove 失敗:

  • 攔截到資料庫例外狀況。
  • [刪除] 頁面的 OnGetAsync 方法會使用 saveChangesError=true 來呼叫。

將錯誤訊息新增至 [刪除 Razor Page] (Pages/Students/Delete.cshtml):

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
    </dl>

    <form method="post">
        <input type="hidden" asp-for="Student.ID" />
        <input type="submit" value="Delete" class="btn btn-danger" /> |
        <a asp-page="./Index">Back to List</a>
    </form>
</div>

執行應用程式並刪除學生以測試 [刪除] 頁面。

下一步