教學課程:使用 EF Core 處理並行 ASP.NET MVCTutorial: Handle concurrency - ASP.NET MVC with EF Core

在先前的教學課程中,您學會了如何更新資料。In earlier tutorials, you learned how to update data. 本教學課程會顯示如何在多位使用者同時更新相同實體時處理衝突。This tutorial shows how to handle conflicts when multiple users update the same entity at the same time.

您會建立操作 Department 實體的網頁,並處理並行錯誤。You'll create web pages that work with the Department entity and handle concurrency errors. 下列圖例顯示了 [編輯] 和 [刪除] 頁面,包括一些發生並行衝突時會顯示的訊息。The following illustrations show the Edit and Delete pages, including some messages that are displayed if a concurrency conflict occurs.

Department [編輯] 頁面

Department [刪除] 頁面

在本教學課程中,您:In this tutorial, you:

  • 了解並行衝突Learn about concurrency conflicts
  • 新增追蹤屬性Add a tracking property
  • 建立 Departments 控制器和檢視Create Departments controller and views
  • 更新 [索引] 檢視Update Index view
  • 更新 [編輯] 方法Update Edit methods
  • 更新 [編輯] 檢視Update Edit view
  • 測試並行衝突Test concurrency conflicts
  • 更新 [刪除] 頁面Update the Delete page
  • 更新 [詳細資料] 及 [建立] 檢視Update Details and Create views

必要條件Prerequisites

並行衝突Concurrency conflicts

當一名使用者為了編輯而顯示了實體的資料,然後另一名使用者在第一名使用者所作出的變更寫入到資料庫前便更新了相同實體的資料時,便會發生並行衝突。A concurrency conflict occurs when one user displays an entity's data in order to edit it, and then another user updates the same entity's data before the first user's change is written to the database. 若您沒有啟用針對這類衝突的偵測,最後更新資料庫的使用者所作出的變更便會覆寫前一名使用者所作出的變更。If you don't enable the detection of such conflicts, whoever updates the database last overwrites the other user's changes. 在許多應用程式中,這類風險是可接受的:若僅有幾名使用者或僅有幾項更新,或覆寫變更的風險並不是那麼的重大,則為了處理並行而耗費的程式設計成本可能會大於其所能帶來的利益。In many applications, this risk is acceptable: if there are few users, or few updates, or if isn't really critical if some changes are overwritten, the cost of programming for concurrency might outweigh the benefit. 在此情況下,您便不需要設定應用程式來處理並行衝突。In that case, you don't have to configure the application to handle concurrency conflicts.

封閉式並行存取 (鎖定)Pessimistic concurrency (locking)

若您的應用程式確實需要防止在並行案例下發生的意外資料遺失,其中一個方法便是使用資料庫鎖定。If your application does need to prevent accidental data loss in concurrency scenarios, one way to do that is to use database locks. 這稱之為封閉式並行存取。This is called pessimistic concurrency. 例如,在您從資料庫讀取一個資料列之前,您會要求唯讀鎖定或更新存取鎖定。For example, before you read a row from a database, you request a lock for read-only or for update access. 若您鎖定了一個資料列以進行更新存取,其他使用者便無法為了唯讀或更新存取而鎖定該資料列,因為他們會取得一個正在進行變更之資料的複本。If you lock a row for update access, no other users are allowed to lock the row either for read-only or update access, because they would get a copy of data that's in the process of being changed. 若您鎖定資料列以進行唯讀存取,其他使用者也可以為了唯讀存取將其鎖定,但無法進行更新。If you lock a row for read-only access, others can also lock it for read-only access but not for update.

管理鎖定有幾個缺點。Managing locks has disadvantages. 其程式可能相當複雜。It can be complex to program. 這需要大量的資料庫管理資源,並且可能會隨著應用程式使用者的數量提升而導致效能問題。It requires significant database management resources, and it can cause performance problems as the number of users of an application increases. 基於這些理由,不是所有的資料庫管理系統都支援封閉式並行存取。For these reasons, not all database management systems support pessimistic concurrency. Entity Framework Core 並未提供內建支援,並且此教學課程也不會教導您如何實作封閉式並行存取。Entity Framework Core provides no built-in support for it, and this tutorial doesn't show you how to implement it.

開放式並行存取Optimistic Concurrency

封閉式並行存取的替代方案便是開放式並行存取。The alternative to pessimistic concurrency is optimistic concurrency. 開放式並行存取表示允許並行衝突發生,然後在衝突發生時適當的做出反應。Optimistic concurrency means allowing concurrency conflicts to happen, and then reacting appropriately if they do. 例如,Jane 造訪了 Department [編輯] 頁面,然後將英文部門的預算金額從美金 $350,000.00 元調整到美金 $0.00 元。For example, Jane visits the Department Edit page and changes the Budget amount for the English department from $350,000.00 to $0.00.

將預算變更為 0

在 Jane 按一下 [儲存]**** 前,John 造訪了相同的頁面並將 [開始日期] 欄位從 2007/9/1 變更為 2013/9/1。Before Jane clicks Save, John visits the same page and changes the Start Date field from 9/1/2007 to 9/1/2013.

將開始日期變更為 2013 年

Jana 先按了一下 [儲存]****,並且在瀏覽器返回 [索引] 頁面時看到她做出的變更。Jane clicks Save first and sees her change when the browser returns to the Index page.

預算已變更為 0

然後 John 在仍然顯示預算為美金 $350,000.00 的 [編輯] 頁面上按一下 [儲存]****。Then John clicks Save on an Edit page that still shows a budget of $350,000.00. 接下來發生的情況便是由您處理並行衝突的方式決定。What happens next is determined by how you handle concurrency conflicts.

一部分選項包括下列項目:Some of the options include the following:

  • 您可以追蹤使用者修改的屬性,然後僅在資料庫中更新相對應的資料行。You can keep track of which property a user has modified and update only the corresponding columns in the database.

    在範例案例中,將不會發生資料遺失,因為兩名使用者更新的屬性不同。In the example scenario, no data would be lost, because different properties were updated by the two users. 下一次當有人瀏覽英文部門時,他們便會同時看到 Jane 和 John 所作出的變更 -- 開始日期為 2013/9/1,預算為美金 0 元。The next time someone browses the English department, they will see both Jane's and John's changes -- a start date of 9/1/2013 and a budget of zero dollars. 這個更新方法可減少可能導致資料遺失之衝突發生的次數,但卻無法在實體中的相同屬性遭到變更時避免資料遺失。This method of updating can reduce the number of conflicts that could result in data loss, but it can't avoid data loss if competing changes are made to the same property of an entity. Entity Framework 是否會以這種方式處理並行衝突,取決於您實作更新程式碼的方式。Whether the Entity Framework works this way depends on how you implement your update code. 通常在 Web 應用程式中,這種方法並不實用,因為它需要您維持大量的狀態,以追蹤實體所有原始的屬性值和新的值。It's often not practical in a web application, because it can require that you maintain large amounts of state in order to keep track of all original property values for an entity as well as new values. 維護大量的狀態可能會影響應用程式效能,因為它可能需要伺服器資源,或是必須包含在網頁本身 (例如,) 或中的隱藏欄位中 cookie 。Maintaining large amounts of state can affect application performance because it either requires server resources or must be included in the web page itself (for example, in hidden fields) or in a cookie.

  • 您可以讓 John 的變更覆寫 Jane 的變更。You can let John's change overwrite Jane's change.

    下一次當有人瀏覽英文部門時,他們便會看到開始日期為 2013/9/1,且預算的金額已還原到美金 $350,000.00 元。The next time someone browses the English department, they will see 9/1/2013 and the restored $350,000.00 value. 這稱之為「用戶端獲勝 (Client Wins)」** 或「最後寫入為準 (Last in Wins)」** 案例。This is called a Client Wins or Last in Wins scenario. (用戶端的所有值會優先于資料存放區中的內容。如本節簡介中所述 ) ,如果您未對並行處理進行任何編碼,則會自動發生。(All values from the client take precedence over what's in the data store.) As noted in the introduction to this section, if you don't do any coding for concurrency handling, this will happen automatically.

  • 您可以防止 John 的變更更新到資料庫中。You can prevent John's change from being updated in the database.

    一般而言,您會顯示一個錯誤訊息,將資料目前的狀態顯示給他,然後允許他重新套用他所作出的變更 (若他還是要變更的話)。Typically, you would display an error message, show him the current state of the data, and allow him to reapply his changes if he still wants to make them. 這稱為「存放區獲勝 (Store Wins)」** 案例。This is called a Store Wins scenario. (資料存放區的值會優先于用戶端所提交的值。 ) 您將在本教學課程中執行存放區的 Wins 案例。(The data-store values take precedence over the values submitted by the client.) You'll implement the Store Wins scenario in this tutorial. 這個方法可確保沒有任何變更會在使用者收到警示,告知其發生的事情前遭到覆寫。This method ensures that no changes are overwritten without a user being alerted to what's happening.

偵測並行衝突Detecting concurrency conflicts

您可以透過處理 Entity Framework 擲回的 DbConcurrencyException 例外狀況來解析衝突。You can resolve conflicts by handling DbConcurrencyException exceptions that the Entity Framework throws. 若要得知何時應擲回這些例外狀況,Entity Framework 必須能夠偵測衝突。In order to know when to throw these exceptions, the Entity Framework must be able to detect conflicts. 因此,您必須適當的設定資料庫及資料模型。Therefore, you must configure the database and the data model appropriately. 一部分啟用衝突偵測的選項包括下列選項:Some options for enabling conflict detection include the following:

  • 在資料庫資料表中,包含一個追蹤資料行,該資料行可用於決定資料列發生變更的時機。In the database table, include a tracking column that can be used to determine when a row has been changed. 您接著便可以設定 Entity Framework,使其在 SQL Update 或 Delete 命令的 Where 子句中包含該資料行。You can then configure the Entity Framework to include that column in the Where clause of SQL Update or Delete commands.

    追蹤資料行的資料類型通常是 rowversionThe data type of the tracking column is typically rowversion. rowversion 的值是一個循序號碼,每一次資料列更新時都會遞增。The rowversion value is a sequential number that's incremented each time the row is updated. 在 Update 或 Delete 命令中,Where 子句會包含追蹤資料行原始的值 (原始的資料列版本)。In an Update or Delete command, the Where clause includes the original value of the tracking column (the original row version) . 若正在更新的資料列已由其他使用者變更,rowversion 中的值便會與原始的值不同,因此 Update 或 Delete 陳述式便會因為 Where 子句而無法找到資料列進行更新。If the row being updated has been changed by another user, the value in the rowversion column is different than the original value, so the Update or Delete statement can't find the row to update because of the Where clause. 當 Entity Framework 發現 Update 或 Delete 命令沒有更新任何資料列時 (即受影響的資料列數目為 0),它便會將其解譯為並行衝突。When the Entity Framework finds that no rows have been updated by the Update or Delete command (that is, when the number of affected rows is zero), it interprets that as a concurrency conflict.

  • 設定 Entity Framework,使其在 Update 和 Delete 命令中的 Where 子句裡包含資料表中每個資料行原始的值。Configure the Entity Framework to include the original values of every column in the table in the Where clause of Update and Delete commands.

    如同第一個選項,若資料列中在一開始讀取之後有任何資料產生變更,Where 子句便不會傳回任何資料列以進行更新,Entity Framework 便會將其解譯為並行衝突。As in the first option, if anything in the row has changed since the row was first read, the Where clause won't return a row to update, which the Entity Framework interprets as a concurrency conflict. 針對擁有許多資料行的資料庫資料表,此方法可能導致非常龐大的 Where 子句,並且可能會需要您維持龐大數量的狀態。For database tables that have many columns, this approach can result in very large Where clauses, and can require that you maintain large amounts of state. 如前文所述,維持大量的狀態可能會影響應用程式的效能。As noted earlier, maintaining large amounts of state can affect application performance. 因此通常不建議使用這種方法,並且這種方法也不是此教學課程中所使用的方法。Therefore this approach is generally not recommended, and it isn't the method used in this tutorial.

    若您想要實作此方法以處理並行衝突,您必須藉由新增 ConcurrencyCheck 屬性來標記所有實體中您想要追蹤並行的非主索引鍵屬性。If you do want to implement this approach to concurrency, you have to mark all non-primary-key properties in the entity you want to track concurrency for by adding the ConcurrencyCheck attribute to them. 該變更會使 Entity Framework 能在 Update 和 Delete 陳述式的 SQL Where 子句中包含所有資料行。That change enables the Entity Framework to include all columns in the SQL Where clause of Update and Delete statements.

在本教學課程的其餘部分,您會將一個 rowversion 追蹤屬性新增到 Department 實體,建立控制器和檢視,然後進行測試以驗證一切都運作正常。In the remainder of this tutorial you'll add a rowversion tracking property to the Department entity, create a controller and views, and test to verify that everything works correctly.

新增追蹤屬性Add a tracking property

Models/Department.cs 中,新增一個名為 RowVersion 的追蹤屬性:In Models/Department.cs, add a tracking property named 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 子句中。The Timestamp attribute specifies that this column will be included in the Where clause of Update and Delete commands sent to the database. 該屬性稱為 Timestamp,因為先前版本的 SQL Server 在以 SQL rowversion 取代之前使用了 SQL timestamp 資料類型。The attribute is called Timestamp because previous versions of SQL Server used a SQL timestamp data type before the SQL rowversion replaced it. rowversion 的 .NET 類型為位元組陣列。The .NET type for rowversion is a byte array.

若您偏好使用 Fluent API,您可以使用 IsConcurrencyToken 方法 (位於 Data/SchoolContext.cs 中) 來指定追蹤屬性,如以下範例所示:If you prefer to use the fluent API, you can use the IsConcurrencyToken method (in Data/SchoolContext.cs) to specify the tracking property, as shown in the following example:

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

由於新增屬性之後,您也變更了資料庫模型,因此您必須再一次進行移轉。By adding a property you changed the database model, so you need to do another migration.

儲存您的變更並建置專案,然後在命令視窗中輸入以下命令:Save your changes and build the project, and then enter the following commands in the command window:

dotnet ef migrations add RowVersion
dotnet ef database update

建立 Departments 控制器和檢視Create Departments controller and views

Scaffold Departments 控制器和檢視,如同您先前為 Students、Courses 和 Instructors 所做的。Scaffold a Departments controller and views as you did earlier for Students, Courses, and Instructors.

Scaffold Department

在 * DepartmentsController.cs* 檔案中,將四個 "FirstMidName" 變更為 "FullName" ,使部門系統管理員下拉式清單可包含講師的完整名稱,而非只有姓氏。In the DepartmentsController.cs file, change all four occurrences of "FirstMidName" to "FullName" so that the department administrator drop-down lists will contain the full name of the instructor rather than just the last name.

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

更新 [索引] 檢視Update Index view

Scaffolding 引擎會在 [索引] 檢視中建立 RowVersion 資料行,但該欄位不應該顯示出來。The scaffolding engine created a RowVersion column in the Index view, but that field shouldn't be displayed.

請以下列程式碼取代 Views/Departments/Index.cshtml 中的程式碼。Replace the code in Views/Departments/Index.cshtml with the following code.

@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 資料行,並為系統管理員顯示完整的名稱而非只有名字。This changes the heading to "Departments", deletes the RowVersion column, and shows full name instead of first name for the administrator.

更新 [編輯] 方法Update Edit methods

在 HttpGet Edit 方法和 Details 方法中,新增 AsNoTrackingIn both the HttpGet Edit method and the Details method, add AsNoTracking. 在 HttpGet Edit 方法中,為系統管理員新增積極式載入。In the HttpGet Edit method, add eager loading for the Administrator.

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

以下列程式碼取代 HttpPost Edit 方法的現有程式碼:Replace the existing code for the HttpPost Edit method with the following code:

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

程式碼開始時便會嘗試讀取要更新的部門。The code begins by trying to read the department to be updated. FirstOrDefaultAsync 方法傳回 Null,則該部門便已遭其他使用者刪除。If the FirstOrDefaultAsync method returns null, the department was deleted by another user. 在此情況下,程式碼會使用 POST 表單的值建立部門實體,使 [編輯] 頁面仍然可以重新顯示,並加上錯誤訊息。In that case the code uses the posted form values to create a department entity so that the Edit page can be redisplayed with an error message. 或者,若您選擇只顯示錯誤訊息,而不重新顯示部門欄位,則您也可以不需要重新建立部門實體。As an alternative, you wouldn't have to re-create the department entity if you display only an error message without redisplaying the department fields.

檢視會在隱藏欄位中儲存原始的 RowVersion 值,並且此方法會在 rowVersion 參數中接收該值。The view stores the original RowVersion value in a hidden field, and this method receives that value in the rowVersion parameter. 在您呼叫 SaveChanges 之前,您必須將該原始 RowVersion 屬性值放入實體的 OriginalValues 集合中。Before you call SaveChanges, you have to put that original RowVersion property value in the OriginalValues collection for the entity.

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

則當 Entity Framework 建立 SQL UPDATE 命令時,該命令便會包含尋找擁有原始 RowVersion 值之資料列的 WHERE 子句。Then when the Entity Framework creates a SQL UPDATE command, that command will include a WHERE clause that looks for a row that has the original RowVersion value. 若 UPDATE 命令並未影響任何資料列 (即沒有任何資料列具有原始的 RowVersion 值),則 Entity Framework 便會擲回 DbUpdateConcurrencyException 例外狀況。If no rows are affected by the UPDATE command (no rows have the original RowVersion value), the Entity Framework throws a DbUpdateConcurrencyException exception.

針對該例外狀況的 catch 區塊會取得受影響的 Department 實體。該實體具有從例外物件上的 Entries 屬性取得的更新值。The code in the catch block for that exception gets the affected Department entity that has the updated values from the Entries property on the exception object.

var exceptionEntry = ex.Entries.Single();

Entries 集合只會有一個 EntityEntry 物件。The Entries collection will have just one EntityEntry object. 您可以使用該物件來取得由使用者輸入的新值,以及目前資料庫的值。You can use that object to get the new values entered by the user and the current database values.

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

程式碼會為每個資料庫的值與使用者在編輯頁面上輸入的值不同的資料行新增一個自訂錯誤訊息 (為了簡化,這裡只顯示了一個欄位)。The code adds a custom error message for each column that has database values different from what the user entered on the Edit page (only one field is shown here for brevity).

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

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

最後,程式碼會將 departmentToUpdateRowVersion 值設為從資料庫取得的新值。Finally, the code sets the RowVersion value of the departmentToUpdate to the new value retrieved from the database. 這個新的 RowVersion 值會在編輯頁面重新顯示時儲存於隱藏欄位中,並且當下一次使用者按一下 [儲存]**** 時,只有在重新顯示 [編輯] 頁面之後發生的並行錯誤才會被捕捉到。This new RowVersion value will be stored in the hidden field when the Edit page is redisplayed, and the next time the user clicks Save, only concurrency errors that happen since the redisplay of the Edit page will be caught.

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

ModelState.Remove 陳述式是必須的,因為 ModelState 具有舊的 RowVersion 值。The ModelState.Remove statement is required because ModelState has the old RowVersion value. 在檢視中,當兩者同時存在時,欄位的 ModelState 值會優先於模型屬性值。In the view, the ModelState value for a field takes precedence over the model property values when both are present.

更新 [編輯] 檢視Update Edit view

Views/Departments/Edit.cshtml 中,進行下列變更:In Views/Departments/Edit.cshtml, make the following changes:

  • DepartmentID 屬性的隱藏欄位之後立即新增另一個隱藏欄位以儲存 RowVersion 屬性值。Add a hidden field to save the RowVersion property value, immediately following the hidden field for the DepartmentID property.

  • 將 [選取系統管理員] 選項新增到下拉式清單中。Add a "Select Administrator" option to the drop-down list.

@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");}
}

測試並行衝突Test concurrency conflicts

執行應用程式,移至 Departments [索引] 頁面。Run the app and go to the Departments Index page. 以滑鼠右鍵按一下 English 部門的編輯 超連結,然後選取 [開啟新的索引標籤]****,然後按一下 English 部門的編輯超連結。Right-click the Edit hyperlink for the English department and select Open in new tab, then click the Edit hyperlink for the English department. 兩個瀏覽器索引標籤現在會顯示相同的資訊。The two browser tabs now display the same information.

變更第一個瀏覽器索引標籤中的欄位,然後按一下 [儲存]****。Change a field in the first browser tab and click Save.

變更之後的 Department [編輯] 頁面 1

瀏覽器會顯示索引頁面,當中包含了變更之後的值。The browser shows the Index page with the changed value.

變更第二個瀏覽器索引標籤中的欄位。Change a field in the second browser tab.

變更之後的 Department [編輯] 頁面 2

按一下 [儲存]Click Save. 您會看到一個錯誤訊息:You see an error message:

Department [編輯] 頁面錯誤訊息

再按一下 [儲存]****。Click Save again. 您在第二個瀏覽器索引標籤中輸入的值已儲存。The value you entered in the second browser tab is saved. 您會在索引頁面出現時看到儲存的值。You see the saved values when the Index page appears.

更新 [刪除] 頁面Update the Delete page

針對 [刪除] 頁面,Entity Framework 會偵測由其他對部門進行類似編輯的人員所造成的並行衝突。For the Delete page, the Entity Framework detects concurrency conflicts caused by someone else editing the department in a similar manner. 當 HttpGet Delete 方法顯示確認檢視時,檢視會在隱藏欄位中包含原始的 RowVersion 值。When the HttpGet Delete method displays the confirmation view, the view includes the original RowVersion value in a hidden field. 該值接著便可供當使用者確認刪除時所呼叫的 HttpPost Delete 方法使用。That value is then available to the HttpPost Delete method that's called when the user confirms the deletion. 當 Entity Framework 建立 SQL DELETE 命令時,它會包含一個具有原始 RowVersion 值的 WHERE 子句。When the Entity Framework creates the SQL DELETE command, it includes a WHERE clause with the original RowVersion value. 若命令執行的結果為受影響的資料列為零 (即資料列在 [刪除] 確認頁面顯示後發生變更),便會擲回並行衝突例外狀況,並呼叫 HttpGet Delete 方法,並將錯誤旗標設為 true,以在重新顯示的確認頁面上顯示一個錯誤訊息。If the command results in zero rows affected (meaning the row was changed after the Delete confirmation page was displayed), a concurrency exception is thrown, and the HttpGet Delete method is called with an error flag set to true in order to redisplay the confirmation page with an error message. 當受影響的資料列為零時,也有可能是該資料列已由別的使用者刪除;在這種情況下,將不會顯示任何錯誤訊息。It's also possible that zero rows were affected because the row was deleted by another user, so in that case no error message is displayed.

更新 Departments 控制器中的 Delete 方法Update the Delete methods in the Departments controller

DepartmentsController.cs 中,以下列程式碼取代 HttpGet Delete 方法:In DepartmentsController.cs, replace the HttpGet Delete method with the following code:

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

方法會接受一個選用的參數,該參數會指示頁面是否已在發生並行錯誤之後重新顯示。The method accepts an optional parameter that indicates whether the page is being redisplayed after a concurrency error. 若此旗標為 true,且指定的部門已不存在,表示該部門已遭其他使用者刪除。If this flag is true and the department specified no longer exists, it was deleted by another user. 在此情況下,程式碼會重新導向至索引頁面。In that case, the code redirects to the Index page. 若此旗標為 true,但指定的部門仍然存在,表示該部門已遭其他使用者變更。If this flag is true and the Department does exist, it was changed by another user. 在此情況下,程式碼會使用 ViewData 傳送一個錯誤訊息到檢視。In that case, the code sends an error message to the view using ViewData.

以下列程式碼取代 HttpPost Delete 方法 (名為 DeleteConfirmed) 中的程式碼:Replace the code in the HttpPost Delete method (named DeleteConfirmed) with the following code:

[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 程式碼中,此方法僅會接受一個記錄識別碼:In the scaffolded code that you just replaced, this method accepted only a record ID:

public async Task<IActionResult> DeleteConfirmed(int id)

您已將此參數變更為由模型繫結器建立的 Department 實體執行個體。You've changed this parameter to a Department entity instance created by the model binder. 這可讓 EF 除了記錄金鑰外,也能存取 RowVersion 屬性值。This gives EF access to the RowVersion property value in addition to the record key.

public async Task<IActionResult> Delete(Department department)

您也將動作方法的名稱從 DeleteConfirmed 變更為 DeleteYou have also changed the action method name from DeleteConfirmed to Delete. Scaffold 程式碼使用了 DeleteConfirmed 的名稱來給予 HttpPost 方法一個唯一的簽章。The scaffolded code used the name DeleteConfirmed to give the HttpPost method a unique signature. (CLR 需要多載的方法,以擁有不同的方法參數。 ) 簽章是唯一的,您可以使用 MVC 慣例,並針對 HttpPost 和 HttpGet delete 方法使用相同的名稱。(The CLR requires overloaded methods to have different method parameters.) Now that the signatures are unique, you can stick with the MVC convention and use the same name for the HttpPost and HttpGet delete methods.

若部門已遭刪除,AnyAsync 方法會傳回 false,應用程式便會直接返回 Index 方法。If the department is already deleted, the AnyAsync method returns false and the application just goes back to the Index method.

若捕捉到並行錯誤,程式碼會重新顯示刪除確認頁面,並提供一個旗標指示其應顯示並行錯誤訊息。If a concurrency error is caught, the code redisplays the Delete confirmation page and provides a flag that indicates it should display a concurrency error message.

更新 [刪除] 檢視Update the Delete view

Views/Departments/Delete.cshtml 中,以下列新增錯誤訊息欄位和 DepartmentID 及 RowVersion 屬性隱藏欄位的程式碼取代 Scaffold 程式碼。In Views/Departments/Delete.cshtml, replace the scaffolded code with the following code that adds an error message field and hidden fields for the DepartmentID and RowVersion properties. 所做的變更已醒目提示。The changes are highlighted.

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

這會進行下列變更:This makes the following changes:

  • h2h3 標題之間新增一個錯誤訊息。Adds an error message between the h2 and h3 headings.

  • 在 [系統管理員]**** 欄位中將 FirstMidName 取代為 FullName。Replaces FirstMidName with FullName in the Administrator field.

  • 移除 RowVersion 欄位。Removes the RowVersion field.

  • RowVersion 屬性新增一個隱藏欄位。Adds a hidden field for the RowVersion property.

執行應用程式,移至 Departments [索引] 頁面。Run the app and go to the Departments Index page. 以滑鼠右鍵按一下 English 部門的刪除 超連結,然後選取 [開啟新的索引標籤]****,然後在第一個索引標籤中按一下 English 部門的編輯超連結。Right-click the Delete hyperlink for the English department and select Open in new tab, then in the first tab click the Edit hyperlink for the English department.

在第一個視窗中,變更其中一個值,然後按一下 [儲存]****:In the first window, change one of the values, and click Save:

刪除前變更後的 Department [編輯] 頁面

在第二個索引標籤中,按一下 [刪除]****。In the second tab, click Delete. 您會看到並行錯誤訊息,並且 Department 值已根據資料庫中的內容重新整理。You see the concurrency error message, and the Department values are refreshed with what's currently in the database.

Department [刪除] 確認頁面,其中包含了並行錯誤

若您再按一下 [刪除]****,則您將會重新導向至 [索引] 頁面,並且系統將顯示該部門已遭刪除。If you click Delete again, you're redirected to the Index page, which shows that the department has been deleted.

更新 [詳細資料] 及 [建立] 檢視Update Details and Create views

您也可以選擇性的清理 [詳細資料] 和 [建立] 檢視中的 Scaffold 程式碼。You can optionally clean up scaffolded code in the Details and Create views.

取代 Views/Departments/Details.cshtml 中的程式碼以刪除 RowVersion 資料行並顯示系統管理員的完整名稱。Replace the code in Views/Departments/Details.cshtml to delete the RowVersion column and show the full name of the Administrator.

@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 中的程式碼來將 [選取 ] 選項新增到下拉式清單中。Replace the code in Views/Departments/Create.cshtml to add a Select option to the drop-down list.

@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");}
}

取得程式碼Get the code

下載或檢視已完成的應用程式。Download or view the completed application.

其他資源Additional resources

如需如何在 EF Core 中處理並行的詳細資訊,請參閱並行衝突For more information about how to handle concurrency in EF Core, see Concurrency conflicts.

後續步驟Next steps

在本教學課程中,您:In this tutorial, you:

  • 了解並行衝突Learned about concurrency conflicts
  • 新增追蹤屬性Added a tracking property
  • 建立 Departments 控制器和檢視Created Departments controller and views
  • 更新 [索引] 檢視Updated Index view
  • 更新 [編輯] 方法Updated Edit methods
  • 更新 [編輯] 檢視Updated Edit view
  • 測試並行衝突Tested concurrency conflicts
  • 更新 [刪除] 頁面Updated the Delete page
  • 更新 [詳細資料] 和 [建立] 檢視Updated Details and Create views

若要了解如何為 Instructor 和 Student 實體實作依階層建立資料表的繼承,請前往下一個教學課程。Advance to the next tutorial to learn how to implement table-per-hierarchy inheritance for the Instructor and Student entities.