教學課程:處理並行 - ASP.NET MVC 搭配 EF Core

在先前的教學課程中,您學會了如何更新資料。 本教學課程會顯示如何在多位使用者同時更新相同實體時處理衝突。

您會建立操作 Department 實體的網頁,並處理並行錯誤。 下列圖例顯示了 [編輯] 和 [刪除] 頁面,包括一些發生並行衝突時會顯示的訊息。

Department Edit page

Department Delete page

在本教學課程中,您已:

  • 了解並行衝突
  • 新增追蹤屬性
  • 建立 Departments 控制器和檢視
  • 更新 [索引] 檢視
  • 更新 [編輯] 方法
  • 更新 [編輯] 檢視
  • 測試並行衝突
  • 更新 [刪除] 頁面
  • 更新 [詳細資料] 及 [建立] 檢視

必要條件

並行衝突

當一名使用者為了編輯而顯示了實體的資料,然後另一名使用者在第一名使用者所作出的變更寫入到資料庫前便更新了相同實體的資料時,便會發生並行衝突。 若您沒有啟用針對這類衝突的偵測,最後更新資料庫的使用者所作出的變更便會覆寫前一名使用者所作出的變更。 在許多應用程式中,這類風險是可接受的:若僅有幾名使用者或僅有幾項更新,或覆寫變更的風險並不是那麼的重大,則為了處理並行而耗費的程式設計成本可能會大於其所能帶來的利益。 在此情況下,您便不需要設定應用程式來處理並行衝突。

封閉式並行存取 (鎖定)

若您的應用程式確實需要防止在並行案例下發生的意外資料遺失,其中一個方法便是使用資料庫鎖定。 這稱之為封閉式並行存取。 例如,在您從資料庫讀取一個資料列之前,您會要求唯讀鎖定或更新存取鎖定。 若您鎖定了一個資料列以進行更新存取,其他使用者便無法為了唯讀或更新存取而鎖定該資料列,因為他們會取得一個正在進行變更之資料的複本。 若您鎖定資料列以進行唯讀存取,其他使用者也可以為了唯讀存取將其鎖定,但無法進行更新。

管理鎖定有幾個缺點。 其程式可能相當複雜。 這需要大量的資料庫管理資源,並且可能會隨著應用程式使用者的數量提升而導致效能問題。 基於這些理由,不是所有的資料庫管理系統都支援封閉式並行存取。 Entity Framework Core 並未提供內建支援,並且此教學課程也不會教導您如何實作封閉式並行存取。

開放式並行存取

封閉式並行存取的替代方案便是開放式並行存取。 開放式並行存取表示允許並行衝突發生,然後在衝突發生時適當的做出反應。 例如,Jane 造訪了 Department [編輯] 頁面,然後將英文部門的預算金額從美金 $350,000.00 元調整到美金 $0.00 元。

Changing budget to 0

在 Jane 按一下 [儲存] 前,John 造訪了相同的頁面並將 [開始日期] 欄位從 2007/9/1 變更為 2013/9/1。

Changing start date to 2013

Jana 先按了一下 [儲存],並且在瀏覽器返回 [索引] 頁面時看到她做出的變更。

Budget changed to zero

然後 John 在仍然顯示預算為美金 $350,000.00 的 [編輯] 頁面上按一下 [儲存]。 接下來發生的情況便是由您處理並行衝突的方式決定。

一部分選項包括下列項目:

  • 您可以追蹤使用者修改的屬性,然後僅在資料庫中更新相對應的資料行。

    在範例案例中,將不會發生資料遺失,因為兩名使用者更新的屬性不同。 下一次當有人瀏覽英文部門時,他們便會同時看到 Jane 和 John 所作出的變更 -- 開始日期為 2013/9/1,預算為美金 0 元。 這個更新方法可減少可能導致資料遺失之衝突發生的次數,但卻無法在實體中的相同屬性遭到變更時避免資料遺失。 Entity Framework 是否會以這種方式處理並行衝突,取決於您實作更新程式碼的方式。 通常在 Web 應用程式中,這種方法並不實用,因為它需要您維持大量的狀態,以追蹤實體所有原始的屬性值和新的值。 維持大量狀態可能會影響應用程式的效能,因為它不是需要伺服器資源,就是必須包含在網頁中 (例如隱藏欄位),或是保存在 cookie 中。

  • 您可以讓 John 的變更覆寫 Jane 的變更。

    下一次當有人瀏覽英文部門時,他們便會看到開始日期為 2013/9/1,且預算的金額已還原到美金 $350,000.00 元。 這稱之為「用戶端獲勝 (Client Wins)」或「最後寫入為準 (Last in Wins)」案例。 (所有來自用戶端的值都會優先於資料存放區中的資料。)如同本節一開始所描述,若您沒有為並行衝突撰寫任何程式碼,這種情況便會自動發生。

  • 您可以防止 John 的變更更新到資料庫中。

    一般而言,您會顯示一個錯誤訊息,將資料目前的狀態顯示給他,然後允許他重新套用他所作出的變更 (若他還是要變更的話)。 這稱之為「存放區獲勝 (Store Wins)」案例。 (資料存放區的值會優先於用戶端所提交的值。)您將在此教學課程中實作存放區獲勝案例。 這個方法可確保沒有任何變更會在使用者收到警示,告知其發生的事情前遭到覆寫。

偵測並行衝突

您可以透過處理 Entity Framework 擲回的 DbConcurrencyException 例外狀況來解析衝突。 若要得知何時應擲回這些例外狀況,Entity Framework 必須能夠偵測衝突。 因此,您必須適當的設定資料庫及資料模型。 一部分啟用衝突偵測的選項包括下列選項:

  • 在資料庫資料表中,包含一個追蹤資料行,該資料行可用於決定資料列發生變更的時機。 您接著便可以設定 Entity Framework,使其在 SQL Update 或 Delete 命令的 Where 子句中包含該資料行。

    追蹤資料行的資料類型通常是 rowversionrowversion 的值是一個循序號碼,每一次資料列更新時都會遞增。 在 Update 或 Delete 命令中,Where 子句會包含追蹤資料行原始的值 (原始的資料列版本)。 若正在更新的資料列已由其他使用者變更,rowversion 中的值便會與原始的值不同,因此 Update 或 Delete 陳述式便會因為 Where 子句而無法找到資料列進行更新。 當 Entity Framework 發現 Update 或 Delete 命令沒有更新任何資料列時 (即受影響的資料列數目為 0),它便會將其解譯為並行衝突。

  • 設定 Entity Framework,使其在 Update 和 Delete 命令中的 Where 子句裡包含資料表中每個資料行原始的值。

    如同第一個選項,若資料列中在一開始讀取之後有任何資料產生變更,Where 子句便不會傳回任何資料列以進行更新,Entity Framework 便會將其解譯為並行衝突。 針對擁有許多資料行的資料庫資料表,此方法可能導致非常龐大的 Where 子句,並且可能會需要您維持龐大數量的狀態。 如前文所述,維持大量的狀態可能會影響應用程式的效能。 因此通常不建議使用這種方法,並且這種方法也不是此教學課程中所使用的方法。

    若您想要實作此方法以處理並行衝突,您必須藉由新增 ConcurrencyCheck 屬性來標記所有實體中您想要追蹤並行的非主索引鍵屬性。 該變更會使 Entity Framework 能在 Update 和 Delete 陳述式的 SQL Where 子句中包含所有資料行。

在本教學課程的其餘部分,您會將一個 rowversion 追蹤屬性新增到 Department 實體,建立控制器和檢視,然後進行測試以驗證一切都運作正常。

新增追蹤屬性

Models/Department.cs 中,新增名為 RowVersion 的追蹤屬性:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Timestamp 屬性會指定此資料行會包含在傳送到資料庫之 Update 和 Delete 命令的 Where 子句中。 該屬性稱為 Timestamp,因為先前版本的 SQL Server 在以 SQL rowversion 取代之前使用了 SQL timestamp 資料類型。 rowversion 的 .NET 類型為位元組陣列。

若您偏好使用 Fluent API,您可以使用 IsConcurrencyToken 方法 (在 Data/SchoolContext.cs 中) 來指定追蹤屬性,如以下範例所示:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

由於新增屬性之後,您也變更了資料庫模型,因此您必須再一次進行移轉。

儲存您的變更並建置專案,然後在命令視窗中輸入以下命令:

dotnet ef migrations add RowVersion
dotnet ef database update

建立 Departments 控制器和檢視

Scaffold Departments 控制器和檢視,如同您先前為 Students、Courses 和 Instructors 所做的。

Scaffold Department

DepartmentsController.cs 檔案中,將四個 "FirstMidName" 變更為 "FullName",使部門系統管理員下拉式清單可包含講師的完整名稱,而非只有姓氏。

ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", department.InstructorID);

更新 [索引] 檢視

Scaffolding 引擎會在 [索引] 檢視中建立 RowVersion 資料行,但該欄位不應該顯示。

Views/Departments/Index.cshtml 中的程式碼取代為下列程式碼。

@model IEnumerable<ContosoUniversity.Models.Department>

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

<h2>Departments</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Budget)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.StartDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Administrator)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Budget)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.StartDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Administrator.FullName)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.DepartmentID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.DepartmentID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

這會將標題變更為 "Departments",刪除 RowVersion 資料行,並為系統管理員顯示完整的名稱而非只有名字。

更新 [編輯] 方法

在 HttpGet Edit 方法和 Details 方法中,新增 AsNoTracking。 在 HttpGet Edit 方法中,為系統管理員新增積極式載入。

var department = await _context.Departments
    .Include(i => i.Administrator)
    .AsNoTracking()
    .FirstOrDefaultAsync(m => m.DepartmentID == id);

以下列程式碼取代 HttpPost Edit 方法的現有程式碼:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
    if (id == null)
    {
        return NotFound();
    }

    var departmentToUpdate = await _context.Departments.Include(i => i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        await TryUpdateModelAsync(deletedDepartment);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    _context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate,
        "",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var exceptionEntry = ex.Entries.Single();
            var clientValues = (Department)exceptionEntry.Entity;
            var databaseEntry = exceptionEntry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                {
                    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");
                }
                if (databaseValues.Budget != clientValues.Budget)
                {
                    ModelState.AddModelError("Budget", $"Current value: {databaseValues.Budget:c}");
                }
                if (databaseValues.StartDate != clientValues.StartDate)
                {
                    ModelState.AddModelError("StartDate", $"Current value: {databaseValues.StartDate:d}");
                }
                if (databaseValues.InstructorID != clientValues.InstructorID)
                {
                    Instructor databaseInstructor = await _context.Instructors.FirstOrDefaultAsync(i => i.ID == databaseValues.InstructorID);
                    ModelState.AddModelError("InstructorID", $"Current value: {databaseInstructor?.FullName}");
                }

                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
                ModelState.Remove("RowVersion");
            }
        }
    }
    ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

程式碼開始時便會嘗試讀取要更新的部門。 若 FirstOrDefaultAsync 方法傳回 Null,則該部門便已遭其他使用者刪除。 在此情況下,程式碼會使用 POST 表單的值建立 Department 實體,使 [編輯] 頁面仍然可以重新顯示,並加上錯誤訊息。 或者,若您選擇只顯示錯誤訊息,而不重新顯示部門欄位,則您也可以不需要重新建立 Department 實體。

檢視會在隱藏欄位中儲存原始的 RowVersion 值,並且此方法會在 rowVersion 參數中接收該值。 在您呼叫 SaveChanges 之前,您必須將該原始 RowVersion 屬性值放入實體的 OriginalValues 集合中。

_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue = rowVersion;

則當 Entity Framework 建立 SQL UPDATE 命令時,該命令便會包含尋找擁有原始 RowVersion 值之資料列的 WHERE 子句。 若 UPDATE 命令並未影響任何資料列 (即沒有任何資料列具有原始的 RowVersion 值),則 Entity Framework 便會擲回 DbUpdateConcurrencyException 例外狀況。

針對該例外狀況的 catch 區塊會取得受影響的 Department 實體。該實體具有從例外物件上的 Entries 屬性取得的更新值。

var exceptionEntry = ex.Entries.Single();

Entries 集合只會有一個 EntityEntry 物件。 您可以使用該物件來取得由使用者輸入的新值,以及目前資料庫的值。

var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();

程式碼會為每個資料庫的值與使用者在編輯頁面上輸入的值不同的資料行新增一個自訂錯誤訊息 (為了簡化,這裡只顯示了一個欄位)。

var databaseValues = (Department)databaseEntry.ToObject();

if (databaseValues.Name != clientValues.Name)
{
    ModelState.AddModelError("Name", $"Current value: {databaseValues.Name}");

最後,程式碼會將 departmentToUpdateRowVersion 值設為從資料庫取得的新值。 這個新的 RowVersion 值會在編輯頁面重新顯示時儲存於隱藏欄位中,並且當下一次使用者按一下 [儲存] 時,只有在重新顯示 [編輯] 頁面之後發生的並行錯誤才會被捕捉到。

departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");

ModelState.Remove 陳述式是必須的,因為 ModelState 具有舊的 RowVersion 值。 在檢視中,當兩者同時存在時,欄位的 ModelState 值會優先於模型屬性值。

更新 [編輯] 檢視

Views/Departments/Edit.cshtml 中,進行下列變更:

  • DepartmentID 屬性的隱藏欄位之後立即新增另一個隱藏欄位以儲存 RowVersion 屬性值。

  • 將 [選取系統管理員] 選項新增到下拉式清單中。

@model ContosoUniversity.Models.Department

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

<h2>Edit</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="DepartmentID" />
            <input type="hidden" asp-for="RowVersion" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
                <span asp-validation-for="InstructorID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

測試並行衝突

執行應用程式,移至 Departments [索引] 頁面。 以滑鼠右鍵按一下 English 部門的編輯 超連結,然後選取 [開啟新的索引標籤],然後按一下 English 部門的編輯超連結。 兩個瀏覽器索引標籤現在會顯示相同的資訊。

變更第一個瀏覽器索引標籤中的欄位,然後按一下 [儲存]

Department Edit page 1 after change

瀏覽器會顯示索引頁面,當中包含了變更之後的值。

變更第二個瀏覽器索引標籤中的欄位。

Department Edit page 2 after change

按一下 [檔案] 。 您會看到一個錯誤訊息:

Department Edit page error message

再次按一下 [儲存]。 您在第二個瀏覽器索引標籤中輸入的值已儲存。 您會在索引頁面出現時看到儲存的值。

更新 [刪除] 頁面

針對 [刪除] 頁面,Entity Framework 會偵測由其他對部門進行類似編輯的人員所造成的並行衝突。 當 HttpGet Delete 方法顯示確認檢視時,檢視會在隱藏欄位中包含原始的 RowVersion 值。 該值接著便可供當使用者確認刪除時所呼叫的 HttpPost Delete 方法使用。 當 Entity Framework 建立 SQL DELETE 命令時,它會包含一個具有原始 RowVersion 值的 WHERE 子句。 若命令執行的結果為受影響的資料列為零 (即資料列在 [刪除] 確認頁面顯示後發生變更),便會擲回並行衝突例外狀況,並呼叫 HttpGet Delete 方法,並將錯誤旗標設為 true,以在重新顯示的確認頁面上顯示一個錯誤訊息。 當受影響的資料列為零時,也有可能是該資料列已由別的使用者刪除;在這種情況下,將不會顯示任何錯誤訊息。

更新 Departments 控制器中的 Delete 方法

DepartmentsController.cs 中,以下列程式碼取代 HttpGet Delete 方法:

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

    var department = await _context.Departments
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.DepartmentID == id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction(nameof(Index));
        }
        return NotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewData["ConcurrencyErrorMessage"] = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

方法會接受一個選用的參數,該參數會指示頁面是否已在發生並行錯誤之後重新顯示。 若此旗標為 true,且指定的部門已不存在,表示該部門已遭其他使用者刪除。 在此情況下,程式碼會重新導向至索引頁面。 若此旗標為 true,但指定的部門仍然存在,表示該部門已遭其他使用者變更。 在此情況下,程式碼會使用 ViewData 傳送一個錯誤訊息到檢視。

以下列程式碼取代 HttpPost Delete 方法 (名為 DeleteConfirmed) 中的程式碼:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == department.DepartmentID))
        {
            _context.Departments.Remove(department);
            await _context.SaveChangesAsync();
        }
        return RedirectToAction(nameof(Index));
    }
    catch (DbUpdateConcurrencyException /* ex */)
    {
        //Log the error (uncomment ex variable name and write a log.)
        return RedirectToAction(nameof(Delete), new { concurrencyError = true, id = department.DepartmentID });
    }
}

在您剛剛取代的 Scaffold 程式碼中,此方法僅會接受一個記錄識別碼:

public async Task<IActionResult> DeleteConfirmed(int id)

您已將此參數變更為由模型繫結器建立的 Department 實體執行個體。 這可讓 EF 除了記錄金鑰外,也能存取 RowVers`ion 屬性值。

public async Task<IActionResult> Delete(Department department)

您也將動作方法的名稱從 DeleteConfirmed 變更為 Delete。 Scaffold 程式碼使用了 DeleteConfirmed 的名稱來給予 HttpPost 方法一個唯一的簽章。 (CLR 要求多載方法必須要有不同的方法參數。)現在,該簽章已為唯一的簽章,您現在可以遵循 MVC 慣例,然後為 HttpPost 及 HttpGet 刪除方法使用相同的名稱。

若部門已遭刪除,AnyAsync 方法會傳回 false,應用程式便會直接返回 Index 方法。

若捕捉到並行錯誤,程式碼會重新顯示刪除確認頁面,並提供一個旗標指示其應顯示並行錯誤訊息。

更新 [刪除] 檢視

Views/Departments/Delete.cshtml 中,以下列新增錯誤訊息欄位和 DepartmentID 及 RowVersion 屬性隱藏欄位的程式碼取代 Scaffold 程式碼。 所做的變更已醒目提示。

@model ContosoUniversity.Models.Department

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

<h2>Delete</h2>

<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <input type="hidden" asp-for="DepartmentID" />
        <input type="hidden" asp-for="RowVersion" />
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

這會進行下列變更:

  • h2h3 標題之間新增一個錯誤訊息。

  • 在 [系統管理員] 欄位中將 FirstMidName 取代為 FullName。

  • 移除 RowVersion 欄位。

  • RowVersion 屬性新增一個隱藏欄位。

執行應用程式,移至 Departments [索引] 頁面。 以滑鼠右鍵按一下 English 部門的刪除 超連結,然後選取 [開啟新的索引標籤],然後在第一個索引標籤中按一下 English 部門的編輯超連結。

在第一個視窗中,變更其中一個值,然後按一下 [儲存]

Department Edit page after change before delete

在第二個索引標籤中,按一下 [刪除]。 您會看到並行錯誤訊息,並且 Department 值已根據資料庫中的內容重新整理。

Department Delete confirmation page with concurrency error

若您再按一下 [刪除],則您將會重新導向至 [索引] 頁面,並且系統將顯示該部門已遭刪除。

更新 [詳細資料] 及 [建立] 檢視

您也可以選擇性的清理 [詳細資料] 和 [建立] 檢視中的 Scaffold 程式碼。

取代 Views/Departments/Details.cshtml 中的程式碼,以刪除 RowVersion 資料行並顯示系統管理員的完整名稱。

@model ContosoUniversity.Models.Department

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

<h2>Details</h2>

<div>
    <h4>Department</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Budget)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Budget)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.StartDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Administrator)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

取代 Views/Departments/Create.cshtml 中的程式碼,將 [選取] 選項新增到下拉式清單中。

@model ContosoUniversity.Models.Department

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

<h2>Create</h2>

<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Budget" class="control-label"></label>
                <input asp-for="Budget" class="form-control" />
                <span asp-validation-for="Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="StartDate" class="control-label"></label>
                <input asp-for="StartDate" class="form-control" />
                <span asp-validation-for="StartDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="InstructorID" class="control-label"></label>
                <select asp-for="InstructorID" class="form-control" asp-items="ViewBag.InstructorID">
                    <option value="">-- Select Administrator --</option>
                </select>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

取得程式碼

下載或檢視已完成的應用程式。

其他資源

如需如何在 EF Core 中處理並行的詳細資訊,請參閱並行衝突

下一步

在本教學課程中,您已:

  • 了解並行衝突
  • 新增追蹤屬性
  • 建立 Departments 控制器和檢視
  • 更新 [索引] 檢視
  • 更新 [編輯] 方法
  • 更新 [編輯] 檢視
  • 測試並行衝突
  • 更新 [刪除] 頁面
  • 更新 [詳細資料] 和 [建立] 檢視

若要了解如何為 Instructor 和 Student 實體實作依階層建立資料表的繼承,請前往下一個教學課程。