ASP.NET Core 中的 Razor 頁面和 EF Core - 并發(fā)

2019-04-17 08:58 更新

本教程介紹如何處理多個用戶并發(fā)更新同一實(shí)體(同時)時出現(xiàn)的沖突。 如果遇到無法解決的問題,請下載或查看已完成的應(yīng)用。 下載說明。

并發(fā)沖突

在以下情況下,會發(fā)生并發(fā)沖突:

  • 用戶導(dǎo)航到實(shí)體的編輯頁面。
  • 第一個用戶的更改還未寫入數(shù)據(jù)庫之前,另一用戶更新同一實(shí)體。

如果未啟用并發(fā)檢測,當(dāng)發(fā)生并發(fā)更新時:

  • 最后一個更新優(yōu)先。 也就是最后一個更新的值保存至數(shù)據(jù)庫。
  • 第一個并發(fā)更新將會丟失。

開放式并發(fā)

樂觀并發(fā)允許發(fā)生并發(fā)沖突,并在并發(fā)沖突發(fā)生時作出正確反應(yīng)。 例如,Jane 訪問院系編輯頁面,將英語系的預(yù)算從 350,000.00 美元更改為 0.00 美元。

將預(yù)算更改為零

在 Jane 單擊“保存”之前,John 訪問了相同頁面,并將開始日期字段從 2007/1/9 更改為 2013/1/9。

將開始日期更改為 2013

Jane 先單擊“保存”,并在瀏覽器顯示索引頁時看到她的更改。

預(yù)算已更改為零

John 單擊“編輯”頁面上的“保存”,但頁面的預(yù)算仍顯示為 350,000.00 美元。 接下來的情況取決于并發(fā)沖突的處理方式。

樂觀并發(fā)包括以下選項(xiàng):

  • 可以跟蹤用戶已修改的屬性,并僅更新數(shù)據(jù)庫中相應(yīng)的列。在這種情況下,數(shù)據(jù)不會丟失。 兩個用戶更新了不同的屬性。 下次有人瀏覽英語系時,將看到 Jane 和 John 兩個人的更改。 這種更新方法可以減少導(dǎo)致數(shù)據(jù)丟失的沖突數(shù)。 這種方法:無法避免數(shù)據(jù)丟失,如果對同一屬性進(jìn)行競爭性更改的話。通常不適用于 Web 應(yīng)用。 它需要維持重要狀態(tài),以便跟蹤所有提取值和新值。 維持大量狀態(tài)可能影響應(yīng)用性能。可能會增加應(yīng)用復(fù)雜性(與實(shí)體上的并發(fā)檢測相比)。
  • 可讓 John 的更改覆蓋 Jane 的更改。下次有人瀏覽英語系時,將看到 2013/9/1 和提取的值 350,000.00 美元。 這種方法稱為“客戶端優(yōu)先”或“最后一個優(yōu)先”方案。 (客戶端的所有值優(yōu)先于數(shù)據(jù)存儲的值。)如果不對并發(fā)處理進(jìn)行任何編碼,則自動執(zhí)行“客戶端優(yōu)先”。
  • 可以阻止在數(shù)據(jù)庫中更新 John 的更改。 應(yīng)用通常會:顯示錯誤消息。顯示數(shù)據(jù)的當(dāng)前狀態(tài)。允許用戶重新應(yīng)用更改。這稱為“存儲優(yōu)先”方案。 (數(shù)據(jù)存儲值優(yōu)先于客戶端提交的值。)本教程實(shí)施“存儲優(yōu)先”方案。此方法可確保用戶在未收到警報(bào)時不會覆蓋任何更改。

處理并發(fā)

當(dāng)屬性配置為并發(fā)令牌時:

數(shù)據(jù)庫和數(shù)據(jù)模型必須配置為支持引發(fā) DbUpdateConcurrencyException。

檢測屬性的并發(fā)沖突

可使用 ConcurrencyCheck 特性在屬性級別檢測并發(fā)沖突。 該特性可應(yīng)用于模型上的多個屬性。 有關(guān)詳細(xì)信息,請參閱數(shù)據(jù)注釋 - ConcurrencyCheck

本教程中不使用 [ConcurrencyCheck] 特性。

檢測行的并發(fā)沖突

要檢測并發(fā)沖突,請將 rowversion 跟蹤列添加到模型。 rowversion:

  • 是 SQL Server 特定的。 其他數(shù)據(jù)庫可能無法提供類似功能。
  • 用于確定從數(shù)據(jù)庫提取實(shí)體后未更改實(shí)體。

數(shù)據(jù)庫生成 rowversion 序號,該數(shù)字隨著每次行的更新遞增。 在 Update 或 Delete 命令中,Where 子句包括 rowversion 的提取值。 如果要更新的行已更改:

  • rowversion 不匹配提取值。
  • Update 或 Delete 命令不能找到行,因?yàn)?nbsp;Where 子句包含提取的 rowversion。
  • 引發(fā)一個 DbUpdateConcurrencyException。

在 EF Core 中,如果未通過 Update 或 Delete 命令更新行,則引發(fā)并發(fā)異常。

向 Department 實(shí)體添加跟蹤屬性

在 Models/Department.cs 中,添加名為 RowVersion 的跟蹤屬性:

C#

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,因?yàn)橹鞍姹镜?SQL Server 在 SQL rowversion 類型將其替換之前使用 SQL timestamp 數(shù)據(jù)類型。

Fluent API 還可指定跟蹤屬性:

C#

modelBuilder.Entity<Department>()
  .Property<byte[]>("RowVersion")
  .IsRowVersion();

以下代碼顯示更新 Department 名稱時由 EF Core 生成的部分 T-SQL:

SQL

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

前面突出顯示的代碼顯示包含 RowVersion 的 WHERE 子句。 如果數(shù)據(jù)庫 RowVersion 不等于 RowVersion 參數(shù)(@p2),則不更新行。

以下突出顯示的代碼顯示驗(yàn)證更新哪一行的 T-SQL:

SQL

SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

@@ROWCOUNT 返回受上一語句影響的行數(shù)。 在沒有行更新的情況下,EF Core 引發(fā) DbUpdateConcurrencyException。

在 Visual Studio 的輸出窗口中可看見 EF Core 生成的 T-SQL。

更新數(shù)據(jù)庫

添加 RowVersion 屬性可更改數(shù)據(jù)庫模型,這需要遷移。

生成項(xiàng)目。 在命令窗口中輸入以下命令:

console

dotnet ef migrations add RowVersion
dotnet ef database update

前面的命令:

  • 添加 Migrations/{time stamp}_RowVersion.cs 遷移文件。
  • 更新 Migrations/SchoolContextModelSnapshot.cs 文件。 此次更新將以下突出顯示的代碼添加到 BuildModel 方法:C#復(fù)制modelBuilder.Entity("ContosoUniversity.Models.Department", b => { b.Property<int>("DepartmentID") .ValueGeneratedOnAdd(); b.Property<decimal>("Budget") .HasColumnType("money"); b.Property<int?>("InstructorID"); b.Property<string>("Name") .HasMaxLength(50); b.Property<byte[]>("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate(); b.Property<DateTime>("StartDate"); b.HasKey("DepartmentID"); b.HasIndex("InstructorID"); b.ToTable("Department"); });
  • 運(yùn)行遷移以更新數(shù)據(jù)庫。

構(gòu)架院系模型

按照為“學(xué)生”模型搭建基架中的說明操作,并對模型類使用 Department。

上述命令為 Department 模型創(chuàng)建基架。 在 Visual Studio 中打開項(xiàng)目。

生成項(xiàng)目。

更新院系索引頁

基架引擎為索引頁創(chuàng)建 RowVersion 列,但不應(yīng)顯示該字段。 本教程中顯示 RowVersion 的最后一個字節(jié),以幫助理解并發(fā)。 不能保證最后一個字節(jié)是唯一的。 實(shí)際應(yīng)用不會顯示 RowVersion 或 RowVersion 的最后一個字節(jié)。

更新索引頁:

  • 用院系替換索引。
  • 將包含 RowVersion 的標(biāo)記替換為 RowVersion 的最后一個字節(jié)。
  • 將 FirstMidName 替換為 FullName。

以下標(biāo)記顯示更新后的頁面:

HTML

@page
@model ContosoUniversity.Pages.Departments.IndexModel

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

<h2>Departments</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].Budget)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Department[0].StartDate)
                </th>
            <th>
                @Html.DisplayNameFor(model => model.Department[0].Administrator)
            </th>
            <th>
                RowVersion
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Department) {
        <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>
                @item.RowVersion[7]
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

更新編輯頁模型

使用以下代碼更新 pages\departments\edit.cshtml.cs:

C#

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

namespace ContosoUniversity.Pages.Departments
{
    public class EditModel : PageModel
    {
        private readonly ContosoUniversity.Data.SchoolContext _context;

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

        [BindProperty]
        public Department Department { get; set; }
        // Replace ViewData["InstructorID"] 
        public SelectList InstructorNameSL { get; set; }

        public async Task<IActionResult> OnGetAsync(int id)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)  // eager loading
                .AsNoTracking()                 // tracking not required
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

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

            // Use strongly typed data rather than ViewData.
            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FirstMidName");

            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

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

            // null means Department was deleted by another user.
            if (departmentToUpdate == null)
            {
                return await HandleDeletedDepartment();
            }

            // Update the RowVersion to the value when this entity was
            // fetched. If the entity has been updated after it was
            // fetched, RowVersion won't match the DB RowVersion and
            // a DbUpdateConcurrencyException is thrown.
            // A second postback will make them match, unless a new 
            // concurrency issue happens.
            _context.Entry(departmentToUpdate)
                .Property("RowVersion").OriginalValue = Department.RowVersion;

            if (await TryUpdateModelAsync<Department>(
                departmentToUpdate,
                "Department",
                s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToPage("./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. " +
                            "The department was deleted by another user.");
                        return Page();
                    }

                    var dbValues = (Department)databaseEntry.ToObject();
                    await setDbErrorMessage(dbValues, clientValues, _context);

                    // Save the current RowVersion so next postback
                    // matches unless an new concurrency issue happens.
                    Department.RowVersion = (byte[])dbValues.RowVersion;
                    // Must clear the model error for the next postback.
                    ModelState.Remove("Department.RowVersion");
                }
            }

            InstructorNameSL = new SelectList(_context.Instructors,
                "ID", "FullName", departmentToUpdate.InstructorID);

            return Page();
        }

       private async Task<IActionResult> HandleDeletedDepartment()
        {
            Department deletedDepartment = new Department();
            // ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
            ModelState.AddModelError(string.Empty,
                "Unable to save. The department was deleted by another user.");
            InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID); 
            return Page();
        }

        private async Task setDbErrorMessage(Department dbValues,
                Department clientValues, SchoolContext context)
        {

            if (dbValues.Name != clientValues.Name)
            {
                ModelState.AddModelError("Department.Name",
                    $"Current value: {dbValues.Name}");
            }
            if (dbValues.Budget != clientValues.Budget)
            {
                ModelState.AddModelError("Department.Budget",
                    $"Current value: {dbValues.Budget:c}");
            }
            if (dbValues.StartDate != clientValues.StartDate)
            {
                ModelState.AddModelError("Department.StartDate",
                    $"Current value: {dbValues.StartDate:d}");
            }
            if (dbValues.InstructorID != clientValues.InstructorID)
            {
                Instructor dbInstructor = await _context.Instructors
                   .FindAsync(dbValues.InstructorID);
                ModelState.AddModelError("Department.InstructorID",
                    $"Current value: {dbInstructor?.FullName}");
            }

            ModelState.AddModelError(string.Empty,
                "The record you attempted to edit "
              + "was modified by another user after you. 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.");
        }
    }
}

要檢測并發(fā)問題,請使用來自所提取實(shí)體的 rowVersion 值更新 OriginalValue。 EF Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 命令。 如果沒有行受到 UPDATE 命令影響(沒有行具有原始 RowVersion 值),將引發(fā) DbUpdateConcurrencyException 異常。

C#

public async Task<IActionResult> OnPostAsync(int id)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

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

    // null means Department was deleted by another user.
    if (departmentToUpdate == null)
    {
        return await HandleDeletedDepartment();
    }

    // Update the RowVersion to the value when this entity was
    // fetched. If the entity has been updated after it was
    // fetched, RowVersion won't match the DB RowVersion and
    // a DbUpdateConcurrencyException is thrown.
    // A second postback will make them match, unless a new 
    // concurrency issue happens.
    _context.Entry(departmentToUpdate)
        .Property("RowVersion").OriginalValue = Department.RowVersion;

在前面的代碼中,Department.RowVersion 為實(shí)體提取后的值。 使用此方法調(diào)用 FirstOrDefaultAsync 時,OriginalValue 為數(shù)據(jù)庫中的值。

以下代碼獲取客戶端值(向此方法發(fā)布的值)和數(shù)據(jù)庫值:

C#

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./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. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await setDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

以下代碼為每列添加自定義錯誤消息,這些列中的數(shù)據(jù)庫值與發(fā)布到 OnPostAsync 的值不同:

C#

private async Task setDbErrorMessage(Department dbValues,
        Department clientValues, SchoolContext context)
{

    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    if (dbValues.StartDate != clientValues.StartDate)
    {
        ModelState.AddModelError("Department.StartDate",
            $"Current value: {dbValues.StartDate:d}");
    }
    if (dbValues.InstructorID != clientValues.InstructorID)
    {
        Instructor dbInstructor = await _context.Instructors
           .FindAsync(dbValues.InstructorID);
        ModelState.AddModelError("Department.InstructorID",
            $"Current value: {dbInstructor?.FullName}");
    }

    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit "
      + "was modified by another user after you. 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.");
}

以下突出顯示的代碼將 RowVersion 值設(shè)置為從數(shù)據(jù)庫檢索的新值。 用戶下次單擊“保存”時,將僅捕獲最后一次顯示編輯頁后發(fā)生的并發(fā)錯誤。

C#

try
{
    await _context.SaveChangesAsync();
    return RedirectToPage("./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. " +
            "The department was deleted by another user.");
        return Page();
    }

    var dbValues = (Department)databaseEntry.ToObject();
    await setDbErrorMessage(dbValues, clientValues, _context);

    // Save the current RowVersion so next postback
    // matches unless an new concurrency issue happens.
    Department.RowVersion = (byte[])dbValues.RowVersion;
    // Must clear the model error for the next postback.
    ModelState.Remove("Department.RowVersion");
}

ModelState 具有舊的 RowVersion 值,因此需使用 ModelState.Remove 語句。 在 Razor 頁面中,當(dāng)兩者都存在時,字段的 ModelState 值優(yōu)于模型屬性值。

更新“編輯”頁

使用以下標(biāo)記更新 Pages/Departments/Edit.cshtml:

HTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
    ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Department.DepartmentID" />
            <input type="hidden" asp-for="Department.RowVersion" />
            <div class="form-group">
                <label>RowVersion</label>
                @Model.Department.RowVersion[7]
            </div>
            <div class="form-group">
                <label asp-for="Department.Name" class="control-label"></label>
                <input asp-for="Department.Name" class="form-control" />
                <span asp-validation-for="Department.Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.Budget" class="control-label"></label>
                <input asp-for="Department.Budget" class="form-control" />
                <span asp-validation-for="Department.Budget" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Department.StartDate" class="control-label"></label>
                <input asp-for="Department.StartDate" class="form-control" />
                <span asp-validation-for="Department.StartDate" class="text-danger">
                </span>
            </div>
            <div class="form-group">
                <label class="control-label">Instructor</label>
                <select asp-for="Department.InstructorID" class="form-control"
                        asp-items="@Model.InstructorNameSL"></select>
                <span asp-validation-for="Department.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-page="./Index">Back to List</a>
</div>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

前面的標(biāo)記:

  • 將 page 指令從 @page 更新為 @page "{id:int}"。
  • 添加隱藏的行版本。 必須添加 RowVersion,以便回發(fā)綁定值。
  • 顯示 RowVersion 的最后一個字節(jié)以進(jìn)行調(diào)試。
  • 將 ViewData 替換為強(qiáng)類型 InstructorNameSL。

使用編輯頁測試并發(fā)沖突

在英語系打開編輯的兩個瀏覽器實(shí)例:

  • 運(yùn)行應(yīng)用,然后選擇“院系”。
  • 右鍵單擊英語系的“編輯”超鏈接,然后選擇“在新選項(xiàng)卡中打開”。
  • 在第一個選項(xiàng)卡中,單擊英語系的“編輯”超鏈接。

兩個瀏覽器選項(xiàng)卡顯示相同信息。

在第一個瀏覽器選項(xiàng)卡中更改名稱,然后單擊“保存”。

更改后的“院系編輯”頁 1

瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁。 請注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。

在第二個瀏覽器選項(xiàng)卡中更改不同字段。

更改后的“院系編輯”頁 2

單擊“保存” 。 可看見所有不匹配數(shù)據(jù)庫值的字段的錯誤消息:

“院系編輯”頁錯誤消息

此瀏覽器窗口將不會更改名稱字段。 將當(dāng)前值(語言)復(fù)制并粘貼到名稱字段。 退出選項(xiàng)卡。客戶端驗(yàn)證將刪除錯誤消息。

“院系編輯”頁錯誤消息

再次單擊“保存”。 保存在第二個瀏覽器選項(xiàng)卡中輸入的值。 在索引頁中可以看到保存的值。

更新“刪除”頁

使用以下代碼更新“刪除”頁模型:

C#

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

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

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

        [BindProperty]
        public Department Department { get; set; }
        public string ConcurrencyErrorMessage { get; set; }

        public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
        {
            Department = await _context.Departments
                .Include(d => d.Administrator)
                .AsNoTracking()
                .FirstOrDefaultAsync(m => m.DepartmentID == id);

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

            if (concurrencyError.GetValueOrDefault())
            {
                ConcurrencyErrorMessage = "The record you attempted to delete "
                  + "was modified by another user after you selected delete. "
                  + "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.";
            }
            return Page();
        }

        public async Task<IActionResult> OnPostAsync(int id)
        {
            try
            {
                if (await _context.Departments.AnyAsync(
                    m => m.DepartmentID == id))
                {
                    // Department.rowVersion value is from when the entity
                    // was fetched. If it doesn't match the DB, a
                    // DbUpdateConcurrencyException exception is thrown.
                    _context.Departments.Remove(Department);
                    await _context.SaveChangesAsync();
                }
                return RedirectToPage("./Index");
            }
            catch (DbUpdateConcurrencyException)
            {
                return RedirectToPage("./Delete",
                    new { concurrencyError = true, id = id });
            }
        }
    }
}

刪除頁檢測提取實(shí)體并更改時的并發(fā)沖突。 提取實(shí)體后,Department.RowVersion 為行版本。 EF Core 創(chuàng)建 SQL DELETE 命令時,它包括具有 RowVersion 的 WHERE 子句。 如果 SQL DELETE 命令導(dǎo)致零行受影響:

  • SQL DELETE 命令中的 RowVersion 與數(shù)據(jù)庫中的 RowVersion 不匹配。
  • 引發(fā) DbUpdateConcurrencyException 異常。
  • 使用 concurrencyError 調(diào)用 OnGetAsync。

更新“刪除”頁

使用以下代碼更新 Pages/Departments/Delete.cshtml:

HTML

@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel

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

<h2>Delete</h2>

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

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

上述標(biāo)記進(jìn)行以下更改:

  • 將 page 指令從 @page 更新為 @page "{id:int}"。
  • 添加錯誤消息。
  • 將“管理員”字段中的 FirstMidName 替換為 FullName。
  • 更改 RowVersion 以顯示最后一個字節(jié)。
  • 添加隱藏的行版本。 必須添加 RowVersion,以便回發(fā)綁定值。

使用刪除頁測試并發(fā)沖突

創(chuàng)建測試系。

在測試系打開刪除的兩個瀏覽器實(shí)例:

  • 運(yùn)行應(yīng)用,然后選擇“院系”。
  • 右鍵單擊測試系的“刪除”超鏈接,然后選擇“在新選項(xiàng)卡中打開”。
  • 單擊測試系的“編輯”超鏈接。

兩個瀏覽器選項(xiàng)卡顯示相同信息。

在第一個瀏覽器選項(xiàng)卡中更改預(yù)算,然后單擊“保存”。

瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁。 請注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。

從第二個選項(xiàng)卡中刪除測試部門。并發(fā)錯誤顯示來自數(shù)據(jù)庫的當(dāng)前值。 單擊“刪除”將刪除實(shí)體,除非 RowVersion 已更新,院系已刪除。

請參閱繼承了解如何繼承數(shù)據(jù)模型。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號