チュートリアル: コンカレンシーの処理 - ASP.NET MVC と EF Core

先のチュートリアルでは、データを更新する方法について学習しました。 このチュートリアルでは、複数のユーザーが同じエンティティを同時に更新するときの競合の処理方法について説明します。

Department エンティティを使用する Web ページを作成し、コンカレンシー エラーを処理します。 次の図は Edit ページと Delete ページのものです。コンカレンシーで競合が発生すると、メッセージが表示されます。

Department Edit page

Department Delete page

このチュートリアルでは、次の作業を行いました。

  • コンカレンシーの競合について学習する
  • トラッキング プロパティを追加する
  • Departments のコントローラーとビューを作成する
  • Index ビューを更新する
  • Edit メソッドを更新する
  • Edit ビューを更新する
  • コンカレンシーの競合をテストする
  • [削除] ページを更新する
  • Details ビューと Create ビューの更新

必須コンポーネント

コンカレンシーの競合

あるユーザーがあるエンティティのデータを編集目的で表示したとき、別のユーザーが同じエンティティのデータを最初のユーザーの変更がデータベースに書き込まれる前に更新すると、コンカレンシーの競合が発生します。 このような競合の検出を有効にしないと、最後にデータベースを更新したユーザーが他のユーザーの変更を上書きすることになります。 多くのアプリケーションでは、このリスクが許容されています。ユーザーや更新がわずかであれば、あるいは変更が一部上書きされても大きな問題なければ、コンカレンシーのプログラミングにかかるコストが利点よりも重視されることがあります。 その場合、コンカレンシーの競合を処理するようにアプリケーションを構成する必要はありません。

ペシミスティック コンカレンシー (ロック)

コンカレンシーで偶発的にデータが失われる事態をアプリケーションで回避する必要があれば、その方法としてデータベース ロックがあります。 これはペシミスティック コンカレンシーと呼ばれています。 たとえば、データベースから行を読む前に、読み取り専用か更新アクセスでロックを要求します。 更新アクセスで行をロックすると、他のユーザーはその行を読み取り専用または更新アクセスでロックできなくなります。変更中のデータのコピーが与えられるためです。 読み取り専用で行をロックすると、他のユーザーはその行を読み取り専用でロックできますが、更新アクセスではロックできません。

ロックの利用には短所があります。 プログラムが複雑になります。 相当なデータベース管理リソースが必要になります。アプリケーションの利用者数が増えると、パフォーマンス上の問題を引き起こすことがあります。 そのような理由から、一部のデータベース管理システムはペシミスティック コンカレンシーに対応していません。 Entity Framework Core にも組み込まれておらず、このチュートリアルでは実装方法を説明しません。

オプティミスティック コンカレンシー

ペシミスティック コンカレンシーの代わりとなるのがオプティミスティック コンカレンシーです。 オプティミスティック コンカレンシーでは、コンカレンシーの競合の発生を許し、発生したら適切に対処します。 たとえば、Jane が Department Edit ページにアクセスし、English 部署の予算を $350,000.00 から $0.00 に変更するとします。

Changing budget to 0

Jane が [保存] をクリックする前に John が同じページにアクセスし、[開始日] フィールドを 9/1/2007 から 9/1/2013 に変更します。

Changing start date to 2013

Jane が [保存] を先にクリックすると、ブラウザーが Index ページに戻ったとき、Jane の変更が反映されています。

Budget changed to zero

次に John が Edit ページの [保存] をクリックします。このとき、予算は $350,000.00 と表示されています。 この後の動作は、コンカレンシーの競合の処理方法によって決定します。

次のようなオプションがあります。

  • ユーザーが変更したプロパティを追跡記録し、それに該当する列だけをデータベースで更新できます。

    例のシナリオでは、2 人のユーザーが異なるプロパティを更新したため、データは失われません。 今度誰かが English 部署を閲覧すると、Jane の変更と John の変更が両方とも表示されます。開始日が 9/1/2013 で予算が 0 ドルです。 この更新方法では、データの損失につながる可能性がある競合の数を減らすことができますが、あるエンティティの同じプロパティに対して行われた変更が競合する場合、データの損失は避けられません。 Entity Framework がこのように動作するかどうかは、更新コードの実装方法に依存します。 これは Web アプリケーションの場合、実用的ではない場合が多いです。あるエンティティの新しい値に加え、元のプロパティ値もすべて追跡記録するため、大量のステータスを更新することになるからです。 大量のステータスを更新するとなると、サーバー リソースが必要になるか、Web ページ自体 (非表示フィールドなど) や cookie に含める必要があるため、アプリケーションのパフォーマンスに影響が出ます。

  • John の変更で Jane の変更を上書きするように指定できます。

    今度誰かが English 部署を閲覧すると、日付は 9/1/2013 ですが、金額が $350,000.00 に戻っています。 これは Client Wins (クライアント側に合わせる) シナリオまたは Last in Wins (最終書き込み者優先) シナリオと呼ばれています。 (クライアントからの値がすべて、データ ストアの値より優先されます。)このセクションの冒頭でお伝えしたように、コンカレンシー処理について何のコーディングもしない場合、これが自動的に行われます。

  • データベースで John の変更が更新されないようにできます。

    通常、エラー メッセージが表示され、John にデータの現在の状態が伝えられます。John は望むなら変更を再適用できます。 これは Store Wins (ストア側に合わせる) シナリオと呼ばれています。 (クライアントが送信した値よりデータストアの値が優先されます。)このチュートリアルでは、Store Wins シナリオを実装します。 この手法では、変更が上書きされるとき、それが必ずユーザーに警告されます。

コンカレンシーの競合の検出

Entity Framework がスローする DbConcurrencyException 例外を処理することで競合を解決できます。 このような例外がスローされるタイミングを認識する目的で、Entity Framework は競合を検出できなければなりません。 そのため、データベースとデータ モデルを適宜構成する必要があります。 競合検出を有効にするためのオプションには次のようなものがあります。

  • 行が変更されたタイミングを判断するトラッキング列をデータベース テーブルに追加します。 その後、Entity Framework を構成し、SQL の Update または Delete コマンドの Where 句にその列を追加できます。

    トラッキング列のデータ型は通常、rowversion です。 rowversion 値は連続番号であり、行が更新されるたびに増えます。 Update または Delete コマンドでは、Where 句にトラッキング列の元の値が含まれます (元の行バージョン)。 更新中の行が別のユーザーによって変更された場合、rowversion 列の値は元の値とは異なります。Where 句に起因し、Update または Delete ステートメントは更新する行を見つけられません。 Update または Delete コマンドによって行が更新されていないことを Entity Framework が確認すると (影響を受けた行の数が 0 のとき)、それをコンカレンシーの競合として解釈します。

  • Entity Framework を構成し、テーブルで Update コマンドと Delete コマンドの Where 句にすべての列の元の値を追加します。

    最初のオプションと同様に、行が最初に読み取られてから行に変更があった場合、Where 句は更新する行を返さず、Entity Framework はそれをコンカレンシーの競合として解釈します。 データベース テーブルに列がたくさんある場合、この手法では結果的に大量の Where 句が出現し、大量のステータスを保守管理しなければならなくなります。 先に述べたように、大量のステータスを保守管理することになると、アプリケーションのパフォーマンスに影響が出ます。 そのため、この手法は一般的には推奨されません。このチュートリアルでも利用しません。

    コンカレンシーにこの手法を導入する場合、コンカレンシーを追跡記録するエンティティのすべての主キーではないプロパティに ConcurrencyCheck 属性を追加し、印を付ける必要があります。 これで、Entity Framework では、Update ステートメントと Delete ステートメントの SQL Where 句にすべての列を含めることができます。

このチュートリアルの残りの部分では、Department エンティティに rowversion トラッキング プロパティを追加し、コントローラーとビューを作成し、すべてが適切に動作することをテストで確認します。

トラッキング プロパティを追加する

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 句にこの列が追加されます。 前のバージョンの SQL Server では、SQL rowversion に取って代わられる前、SQL timestamp というデータ型が使用されていたため、この属性は 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 のコントローラーとビューを作成する

先に Students、Courses、Instructors に行ったように、Departments のコントローラーとビューをスキャフォールディングします。

Scaffold Department

DepartmentsController.cs ファイルに 4 回登場する "FirstMidName" をすべて "FullName" に変更します。それにより、部署の管理者のドロップダウン リストに講師の姓だけでなく、姓名が表示されます。

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

Index ビューを更新する

スキャフォールディング エンジンによりインデックス ビューに 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 列が削除され、管理者の名ではなく姓名が表示されます。

Edit メソッドを更新する

HttpGet Edit メソッドと Details メソッドの両方に AsNoTracking を追加します。 HttpGet Edit メソッドに Administrator の一括読み込みを追加します。

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 を返した場合、部署は別のユーザーが削除しています。 その場合、このコードは送信されたフォーム値を利用して Department エンティティを作成します。編集ページはエラー メッセージと共に再表示できます。 あるいは、部署フィールドを再表示せず、エラー メッセージのみを表示するのであれば、Department エンティティを再作成する必要はないでしょう。

ビューの非表示フィールドに元の RowVersion 値が保管されます。このメソッドは rowVersion パラメーターでその値を受け取ります。 SaveChanges を呼び出す前に、エンティティの OriginalValues コレクションにその元の RowVersion プロパティ値を置く必要があります。

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

次に、Entity Framework で SQL UPDATE コマンドが作成されるとき、元の RowVersion 値が含まれる行を探す WHERE 句がそのコマンドに含まれます。 UPDATE コマンドの影響を受ける行がない場合 (元の RowVersion 値が含まれる行がない)、Entity Framework は DbUpdateConcurrencyException 例外をスローします。

その例外の catch ブロックのコードによって、影響を受けた、例外オブジェクトの Entries プロパティから更新後の値が与えられた Department エンティティが取得されます。

var exceptionEntry = ex.Entries.Single();

Entries コレクションには EntityEntry オブジェクトが 1 つだけ与えられます。 そのオブジェクトを利用し、ユーザーが入力した新しい値と現在のデータベース値を取得できます。

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

このコードにより、編集ページでユーザーが入力したものとデータベース値が異なる列ごとにカスタムのエラー メッセージが追加されます (簡潔にするため、ここでは 1 つだけフィールドを示しています)。

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

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

最後になりますが、このコードで departmentToUpdateRowVersion 値がデータベースから取得された新しい値に設定されます。 Edit ページが再表示されるとき、この新しい RowVersion 値が非表示フィールドに保存されます。今度ユーザーが [保存] をクリックすると、Edit ページの再表示後に発生したコンカレンシー エラーのみが取得されます。

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

ModelStateRowVersion 値が古いため、ModelState.Remove ステートメントが必要になります。 このビューでは、フィールドの ModelState 値がモデル プロパティ値より優先されます。

Edit ビューを更新する

Views/Departments/Edit.cshtml で、次の変更を行います。

  • DepartmentID プロパティの非表示フィールドのすぐ後ろに RowVersion プロパティ値を保存する非表示フィールドを追加します。

  • ドロップダウン リストに "Select Administrator" オプションを追加します。

@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 Index ページに移動します。 English 部署の [編集] ハイパーリンクを右クリックし、 [新しいタブで開く] を選択し、English 部署の [編集] ハイパーリンクをクリックします。 2 つのブラウザー タブに同じ情報が表示されます。

最初のブラウザー タブでフィールドを変更し、 [保存] をクリックします。

Department Edit page 1 after change

値が変更された Index ページがブラウザーに表示されます。

2 番目のブラウザー タブでフィールドを変更します。

Department Edit page 2 after change

[保存] をクリックします。 エラー メッセージが表示されます。

Department Edit page error message

[保存] をもう一度クリックします。 2 番目のブラウザー タブで入力した値が保存されます。 Index ページが表示されると、保存した値を確認できます。

[削除] ページを更新する

Delete ページの場合、Entity Framework は、同様の方法で部署を編集している他のユーザーが起こしたコンカレンシーの競合を検出します。 HttpGet Delete 値に確定ビューが表示されると、そのビューに非表示フィールドの元の RowVersion 値が含まれます。 その値は、ユーザーが削除を確定したときに呼び出される HttpPost Delete メソッドで利用されます。 Entity Framework で SQL DELETE コマンドが作成されるとき、WHERE 句と元の RowVersion 値が含まれます。 コマンドの結果、影響を受けた行が 0 であれば (削除確認ページの表示後に行が変更された)、コンカレンシー例外がスローされます。確定ページをエラー メッセージと共に再表示するために、エラー フラグが true に設定された状態で HttpGet Delete メソッドが呼び出されます。 別のユーザーが行を削除したため、影響を受けた行が 0 になることもあります。その場合、エラー メッセージは表示されません。

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 のとき、指定の部署が現存していなければ、別のユーザーによって削除されています。 その場合、このコードは Index ページにリダイレクトされます。 このフラグが 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 });
    }
}

置き換えたスキャフォールディングされたコードで、このメソッドがレコード ID を 1 つだけ受け取りました。

public async Task<IActionResult> DeleteConfirmed(int id)

このパラメーターをモデル バインダーによって作成された Department エンティティ インスタンスに変更しています。 それにより、EF はレコード キーに加え、RowVers`ion プロパティ値にアクセスできます。

public async Task<IActionResult> Delete(Department department)

また、アクション メソッドの名前を DeleteConfirmed から Delete に変更しています。 スキャフォールディングされたコードは DeleteConfirmed という名前を利用し、HttpPost メソッドに一意のシグネチャを与えました。 (CLR では、さまざまなメソッド パラメーターを持つために、オーバーロードされたメソッドを必要とします。)シグネチャが一意になっているので、MVC 規則に準拠し、削除メソッドの HttpPost と HttpGet に同じ名前を利用できます。

部署が既に削除されている場合、AnyAsync メソッドは false を返します。アプリケーションは Index メソッドに戻ります。

コンカレンシー エラーがキャッチされた場合、このコードは削除確認ページを再表示し、コンカレンシー エラー メッセージを表示するかどうかを示すフラグを提供します。

Delete ビューを更新する

Views/Departments/Delete.cshtml で、スキャフォールディングされたコードを次のコードで置き換えます。このコードは、DepartmentID プロパティと RowVersion プロパティのエラー メッセージ フィールドと非表示フィールドを追加します。 変更が強調表示されます。

@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 Index ページに移動します。 English 部署の [削除] ハイパーリンクを右クリックし、 [新しいタブで開く] を選択します。次に最初のタブで English 部署の [編集] ハイパーリンクをクリックします。

最初のウィンドウで、いずれかの値を変更し、 [保存] をクリックします。

Department Edit page after change before delete

2 番目のタブで [削除] をクリックします。 コンカレンシー エラー メッセージが表示されます。Department 値がデータベースの現在の内容で更新されています。

Department Delete confirmation page with concurrency error

[削除] をもう一度クリックすると、Index ページにリダイレクトされます。Index ページには、部署が削除されていることが表示されます。

Details ビューと Create ビューの更新

Details ビューと Create ビューで、スキャフォールディングされたコードを任意でクリーンアップできます。

RowVersion 列を削除し、管理者の姓名を表示するように Views/Departments/Details.cshtml のコードを置換します。

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

ドロップダウン リストに Select オプションを追加するように 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 のコントローラーとビューを作成した
  • Index ビューを更新した
  • Edit メソッドを更新した
  • Edit ビューを更新した
  • コンカレンシーの競合をテストした
  • Delete ページを更新した
  • Details および Create ビューを更新した

Instructor および Student エンティティの Table-Per-Hierarchy 継承を実装する方法について学習するには、次のチュートリアルに進んでください。