パート 2、ASP.NET Core での EF Core を使用した Razor ページ - CRUD

作成者: Tom DykstraJeremy LiknessJon P Smith

Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。

解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。

このチュートリアルでは、スキャフォールディング CRUD (作成、読み取り、更新、削除) コードのレビューとカスタマイズを行います。

リポジトリがない

一部の開発者は、サービス レイヤーまたはリポジトリ パターンを使用して、UI (Razor Pages) とデータ アクセス層との間に抽象化レイヤーを作成しています。 このチュートリアルでは、これは行いません。 複雑さを最小限に抑え、チュートリアルの焦点を EF Core に置くために、EF Core コードはページ モデル クラスに直接追加します。

Details ページを更新する

Students ページのスキャフォールディング コードには、登録データが含まれていません。 このセクションでは、Details ページに登録が追加されます。

登録の読み取り

このページに学生の登録データを表示するには、その登録データを読み取る必要があります。 Pages/Students/Details.cshtml.cs のスキャフォールディング コードを使用すると、Enrollment データを含まない Student データのみが読み取られます。

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

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

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

OnGetAsync メソッドを次のコードに置き換えて、選択した学生の登録データを読み取ります。 変更が強調表示されます。

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

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

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

Include および ThenInclude メソッドにより、コンテキストは Student.Enrollments ナビゲーション プロパティと、各登録内の Enrollment.Course ナビゲーション プロパティを読み込みます。 これらのメソッドは、関連データの読み込みのチュートリアルで詳しく検討します。

AsNoTracking メソッドは、返されたエンティティが現在のコンテキストで更新されないシナリオでパフォーマンスを改善します。 AsNoTracking は、このチュートリアルで後述します。

登録の表示

Pages/Students/Details.cshtml 内のコードを次のコードに置き換えて、登録のリストを表示します。 変更が強調表示されます。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

上記のコードは、Enrollments ナビゲーション プロパティ内でエンティティをループ処理します。 登録ごとに、コースのタイトルとグレードが表示されます。 コース タイトルは、Enrollments エンティティの Course ナビゲーション プロパティに格納されている Course エンティティから取得されます。

アプリを実行し、 [Students] タブを選択し、学生用の [詳細] リンクをクリックします。 選択した受講者のコースとグレードの一覧が表示されます。

1 つのエンティティを読み取る方法

生成されたコードでは、FirstOrDefaultAsync を使用して 1 つのエンティティを読み取ります。 このメソッドでは、何も見つからない場合は null が返されます。それ以外の場合は、クエリのフィルター条件を満たす最初の行が返されます。 FirstOrDefaultAsync は、通常、次の代替手段よりも適しています。

  • SingleOrDefaultAsync - クエリ フィルターを満たす複数のエンティティがある場合に、例外をスローします。 クエリによって複数の行が返される可能性があるかどうかを判断するため、SingleOrDefaultAsync は複数の行をフェッチしようとします。 一意のキーを検索する場合と同様に、クエリが 1 つのエンティティだけを返すことができる場合は、この追加作業は不要です。
  • FindAsync - 主キー (PK) を持つエンティティを検索します。 PK を持つエンティティがコンテキストによって追跡されている場合、データベースに対する要求がなくても該当するエンティティが返されます。 このメソッドは、単一のエンティティを検索するように最適化されていますが、FindAsync を使用して Include を呼び出すことはできません。 したがって、関連データが必要な場合は、FirstOrDefaultAsync を選択することをお勧めします。

ルート データとクエリ文字列

Details ページの URL は https://localhost:<port>/Students/Details?id=1 です。 エンティティの主キー値がクエリ文字列に含まれています。 ルート データのキー値を渡すことを好む開発者もいます。https://localhost:<port>/Students/Details/1 詳細については、「生成されたコードの更新」を参照してください。

Create ページを更新する

Create ページのスキャフォールディングされた OnPostAsync コードは、過剰ポスティングに対して脆弱です。 Pages/Students/Create.cshtml.csOnPostAsync メソッドを次のコードに置き換えます。

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

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

    return Page();
}

TryUpdateModelAsync

上記のコードでは、Student オブジェクトを作成し、ポストされたフォーム フィールドを使用して Student オブジェクトのプロパティを更新します。 TryUpdateModelAsync メソッド:

  • PageModelPageContext プロパティからポストされたフォーム値を使用します。
  • リストされたプロパティのみを更新します (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • "student" のプレフィックスを持つフォーム フィールドを検索します。 たとえば、Student.FirstMidName のようにします。 大文字と小文字の区別はありません。
  • モデル バインド システムを使用して、文字列からフォーム値を Student モデル内の型に変換します。 たとえば、EnrollmentDateDateTime に変換されます。

アプリを実行し、Create ページをテストする student エンティティを作成します。

過剰ポスティング

ポストされた値を持つフィールドを更新するために TryUpdateModel を使用することは、過剰ポスティングの防止につながり、セキュリティ上のベスト プラクティスとなります。 たとえば、Student エンティティには、この Web ページで更新または追加できない Secret プロパティが含まれています。

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

アプリの作成または更新の Razor ページに Secret フィールドが含まれていない場合でも、ハッカーは過剰ポスティングによって Secret 値を設定することが可能です。 ハッカーは、Fiddler などのツールを使用するか、または何らかの JavaScript を作成して、Secret フォーム値をポストすることが可能です。 元のコードでは、Student インスタンスの作成時にモデル バインダーによって使用されるフィールドを制限していません。

Secret フォーム フィールドに対してハッカーが指定した値はいずれも、データベース内で更新されます。 次の図は、ポストされたフォームの値に、値 "OverPost" が含まれる Secret フィールドを追加している Fiddler ツールを示しています。

Fiddler adding Secret field

挿入された行の Secret プロパティに値 "OverPost" が正常に追加されています。 これは、アプリ デザイナーで、Create ページで Secret プロパティが設定されることを想定していない場合でも発生します。

ビュー モデル

ビュー モデルは、過剰ポスティングを防ぐもう 1 つの方法を提供します。

アプリケーション モデルは、しばしばドメイン モデルと呼ばれます。 ドメイン モデルには、通常、データベース内の対応するエンティティによって必要とされるすべてのプロパティが含まれています。 ビュー モデルには、Create ページなどの UI ページで必要なプロパティのみが含まれています。

一部のアプリでは、ビュー モデルに加えて、Razor Pages のページ モデル クラスとブラウザーとの間でデータを渡すためにバインド モデルまたは入力モデルも使用します。

次の StudentVM ビュー モデルを考えてみます。

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

次のコードでは、StudentVM ビュー モデルを使用して新しい受講生を作成します。

[BindProperty]
public StudentVM StudentVM { get; set; }

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

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

SetValues メソッドでは、このオブジェクトの値を設定するために、別の PropertyValues オブジェクトから値を読み取ります。 SetValues では一致するプロパティ名が使用されます。 ビューモデルには、次の種類があります。

  • モデルの種類に関連付けられる必要はありません。
  • 一致するプロパティを持つ必要があります。

StudentVM を使用するには、Create ページで、Student ではなく StudentVM を使用する必要があります。

@page
@model CreateVMModel

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

<h1>Create</h1>

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

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

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

[編集] ページを更新する

Pages/Students/Edit.cshtml.cs で、OnGetAsync メソッドと OnPostAsync メソッドを次のコードに置き換えます。

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

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

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

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

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

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

    return Page();
}

コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。

  • FirstOrDefaultAsyncFindAsync に置き換えられました。 関連データを含める必要がない場合は、FindAsync の方が効率的です。
  • OnPostAsync には id パラメーターがあります。
  • 現在の学生は、空の学生を作成するのではなく、データベースからフェッチされます。

アプリを実行し、学生を作成して編集することでアプリをテストします。

エンティティの状態

データベース コンテキストは、メモリ内のエンティティが、データベース内の対応する行と同期状態にあるかどうかを追跡します。 この追跡情報により、SaveChangesAsync が呼び出されたときに何が起こっているかを特定できます。 たとえば、新しいエンティティが AddAsync メソッドに渡されたとき、そのエンティティの状態は Added に設定されます。 SaveChangesAsync が呼び出されると、データベース コンテキストによって SQL の INSERT コマンドが発行されます。

エンティティは、次のいずれかの状態になる可能性があります。

  • Added:エンティティはデータベースにまだ存在しません。 SaveChanges メソッドでは、INSERT ステートメントが発行されます。

  • Unchanged:このエンティティでは変更を保存する必要がありません。 エンティティがこの状態になるのは、エンティティがデータベースから読み取られた場合です。

  • Modified:エンティティのプロパティ値の一部またはすべてが変更されています。 SaveChanges メソッドでは、UPDATE ステートメントが発行されます。

  • Deleted:エンティティには削除のマークが付けられています。 SaveChanges メソッドでは、DELETE ステートメントが発行されます。

  • Detached:エンティティはデータベース コンテキストによって追跡されていません。

デスクトップ アプリにおいて、通常、状態の変更は自動的に設定されます。 エンティティが読み取られ、変更が加えられると、エンティティの状態は自動的に Modified に変更されます。 SaveChanges を呼び出すと、変更されたプロパティのみを更新する SQL UPDATE ステートメントが生成されます。

Web アプリにおいて、エンティティを読み取り、データを表示する DbContext は、ページが表示された後で破棄されます。 ページの OnPostAsync メソッドが呼び出されると、新しい Web 要求が行われ、DbContext の新しいインスタンスが使用されます。 その新しいコンテキスト内のエンティティの再読み取りを行うと、デスクトップの処理がシミュレートされます。

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

このセクションでは、SaveChanges の呼び出しが失敗すると、カスタム エラー メッセージが実装されます。

Pages/Students/Delete.cshtml.cs のコードを次のコードに置き換えます。

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

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

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

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

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

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

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

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

            return Page();
        }

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

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

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

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

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

上記のコードでは次の操作が行われます。

  • ログ記録を追加します。
  • 省略可能なパラメーター saveChangesErrorOnGetAsync メソッド シグネチャに追加します。 saveChangesError は、受講者オブジェクトの削除に失敗した後で、メソッドが呼び出されたかどうかを示します。

一時的なネットワークの問題により、削除操作が失敗する可能性があります。 データベースがクラウド内にある場合は、一時的なネットワーク エラーが発生する可能性が高くなります。 Delete ページの OnGetAsync が UI から呼び出された場合、saveChangesError パラメーターは false です。 削除操作が失敗したために、OnPostAsync によって OnGetAsync が呼び出された場合、saveChangesError パラメーターは true です。

OnPostAsync メソッドは、選択されたエンティティを取得し、Remove メソッドを呼び出して、エンティティの状態を Deleted に設定します。 SaveChanges が呼び出された場合、SQL の DELETE コマンドが生成されます。 Remove が失敗した場合:

  • データベース例外がキャッチされます。
  • [削除] ページの OnGetAsync メソッドが、saveChangesError=true を指定して呼び出されます。

エラー メッセージを Pages/Students/Delete.cshtml に追加します。

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

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

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

アプリを実行し、学生を削除して、Delete ページをテストします。

次の手順

このチュートリアルでは、スキャフォールディング CRUD (作成、読み取り、更新、削除) コードのレビューとカスタマイズを行います。

リポジトリがない

一部の開発者は、サービス レイヤーまたはリポジトリ パターンを使用して、UI (Razor Pages) とデータ アクセス層との間に抽象化レイヤーを作成しています。 このチュートリアルでは、これは行いません。 複雑さを最小限に抑え、チュートリアルの焦点を EF Core に置くために、EF Core コードはページ モデル クラスに直接追加します。

Details ページを更新する

Students ページのスキャフォールディング コードには、登録データが含まれていません。 このセクションでは、Details ページに登録が追加されます。

登録の読み取り

このページに学生の登録データを表示するには、その登録データを読み取る必要があります。 Pages/Students/Details.cshtml.cs のスキャフォールディング コードを使用すると、Enrollment データを含まない Student データのみが読み取られます。

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

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

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

OnGetAsync メソッドを次のコードに置き換えて、選択した学生の登録データを読み取ります。 変更が強調表示されます。

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

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

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

Include および ThenInclude メソッドにより、コンテキストは Student.Enrollments ナビゲーション プロパティと、各登録内の Enrollment.Course ナビゲーション プロパティを読み込みます。 これらのメソッドは、関連データの読み込みのチュートリアルで詳しく検討します。

AsNoTracking メソッドは、返されたエンティティが現在のコンテキストで更新されないシナリオでパフォーマンスを改善します。 AsNoTracking は、このチュートリアルで後述します。

登録の表示

Pages/Students/Details.cshtml 内のコードを次のコードに置き換えて、登録のリストを表示します。 変更が強調表示されます。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

上記のコードは、Enrollments ナビゲーション プロパティ内でエンティティをループ処理します。 登録ごとに、コースのタイトルとグレードが表示されます。 コース タイトルは、Enrollments エンティティの Course ナビゲーション プロパティに格納されている Course エンティティから取得されます。

アプリを実行し、 [Students] タブを選択し、学生用の [詳細] リンクをクリックします。 選択した受講者のコースとグレードの一覧が表示されます。

1 つのエンティティを読み取る方法

生成されたコードでは、FirstOrDefaultAsync を使用して 1 つのエンティティを読み取ります。 このメソッドでは、何も見つからない場合は null が返されます。それ以外の場合は、クエリのフィルター条件を満たす最初の行が返されます。 FirstOrDefaultAsync は、通常、次の代替手段よりも適しています。

  • SingleOrDefaultAsync - クエリ フィルターを満たす複数のエンティティがある場合に、例外をスローします。 クエリによって複数の行が返される可能性があるかどうかを判断するため、SingleOrDefaultAsync は複数の行をフェッチしようとします。 一意のキーを検索する場合と同様に、クエリが 1 つのエンティティだけを返すことができる場合は、この追加作業は不要です。
  • FindAsync - 主キー (PK) を持つエンティティを検索します。 PK を持つエンティティがコンテキストによって追跡されている場合、データベースに対する要求がなくても該当するエンティティが返されます。 このメソッドは、単一のエンティティを検索するように最適化されていますが、FindAsync を使用して Include を呼び出すことはできません。 したがって、関連データが必要な場合は、FirstOrDefaultAsync を選択することをお勧めします。

ルート データとクエリ文字列

Details ページの URL は https://localhost:<port>/Students/Details?id=1 です。 エンティティの主キー値がクエリ文字列に含まれています。 ルート データのキー値を渡すことを好む開発者もいます。https://localhost:<port>/Students/Details/1 詳細については、「生成されたコードの更新」を参照してください。

Create ページを更新する

Create ページのスキャフォールディングされた OnPostAsync コードは、過剰ポスティングに対して脆弱です。 Pages/Students/Create.cshtml.csOnPostAsync メソッドを次のコードに置き換えます。

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

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

    return Page();
}

TryUpdateModelAsync

上記のコードでは、Student オブジェクトを作成し、ポストされたフォーム フィールドを使用して Student オブジェクトのプロパティを更新します。 TryUpdateModelAsync メソッド:

  • PageModelPageContext プロパティからポストされたフォーム値を使用します。
  • リストされたプロパティのみを更新します (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • "student" のプレフィックスを持つフォーム フィールドを検索します。 たとえば、Student.FirstMidName のようにします。 大文字と小文字の区別はありません。
  • モデル バインド システムを使用して、文字列からフォーム値を Student モデル内の型に変換します。 たとえば、EnrollmentDateDateTime に変換されます。

アプリを実行し、Create ページをテストする student エンティティを作成します。

過剰ポスティング

ポストされた値を持つフィールドを更新するために TryUpdateModel を使用することは、過剰ポスティングの防止につながり、セキュリティ上のベスト プラクティスとなります。 たとえば、Student エンティティには、この Web ページで更新または追加できない Secret プロパティが含まれています。

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

アプリの作成または更新の Razor ページに Secret フィールドが含まれていない場合でも、ハッカーは過剰ポスティングによって Secret 値を設定することが可能です。 ハッカーは、Fiddler などのツールを使用するか、または何らかの JavaScript を作成して、Secret フォーム値をポストすることが可能です。 元のコードでは、Student インスタンスの作成時にモデル バインダーによって使用されるフィールドを制限していません。

Secret フォーム フィールドに対してハッカーが指定した値はいずれも、データベース内で更新されます。 次の図は、ポストされたフォームの値に、値 "OverPost" が含まれる Secret フィールドを追加している Fiddler ツールを示しています。

Fiddler adding Secret field

挿入された行の Secret プロパティに値 "OverPost" が正常に追加されています。 これは、アプリ デザイナーで、Create ページで Secret プロパティが設定されることを想定していない場合でも発生します。

ビュー モデル

ビュー モデルは、過剰ポスティングを防ぐもう 1 つの方法を提供します。

アプリケーション モデルは、しばしばドメイン モデルと呼ばれます。 ドメイン モデルには、通常、データベース内の対応するエンティティによって必要とされるすべてのプロパティが含まれています。 ビュー モデルには、Create ページなどの UI ページで必要なプロパティのみが含まれています。

一部のアプリでは、ビュー モデルに加えて、Razor Pages のページ モデル クラスとブラウザーとの間でデータを渡すためにバインド モデルまたは入力モデルも使用します。

次の StudentVM ビュー モデルを考えてみます。

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

次のコードでは、StudentVM ビュー モデルを使用して新しい受講生を作成します。

[BindProperty]
public StudentVM StudentVM { get; set; }

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

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

SetValues メソッドでは、このオブジェクトの値を設定するために、別の PropertyValues オブジェクトから値を読み取ります。 SetValues では一致するプロパティ名が使用されます。 ビューモデルには、次の種類があります。

  • モデルの種類に関連付けられる必要はありません。
  • 一致するプロパティを持つ必要があります。

StudentVM を使用するには、Create ページで、Student ではなく StudentVM を使用する必要があります。

@page
@model CreateVMModel

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

<h1>Create</h1>

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

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

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

[編集] ページを更新する

Pages/Students/Edit.cshtml.cs で、OnGetAsync メソッドと OnPostAsync メソッドを次のコードに置き換えます。

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

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

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

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

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

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

    return Page();
}

コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。

  • FirstOrDefaultAsyncFindAsync に置き換えられました。 関連データを含める必要がない場合は、FindAsync の方が効率的です。
  • OnPostAsync には id パラメーターがあります。
  • 現在の学生は、空の学生を作成するのではなく、データベースからフェッチされます。

アプリを実行し、学生を作成して編集することでアプリをテストします。

エンティティの状態

データベース コンテキストは、メモリ内のエンティティが、データベース内の対応する行と同期状態にあるかどうかを追跡します。 この追跡情報により、SaveChangesAsync が呼び出されたときに何が起こっているかを特定できます。 たとえば、新しいエンティティが AddAsync メソッドに渡されたとき、そのエンティティの状態は Added に設定されます。 SaveChangesAsync が呼び出されると、データベース コンテキストによって SQL の INSERT コマンドが発行されます。

エンティティは、次のいずれかの状態になる可能性があります。

  • Added:エンティティはデータベースにまだ存在しません。 SaveChanges メソッドでは、INSERT ステートメントが発行されます。

  • Unchanged:このエンティティでは変更を保存する必要がありません。 エンティティがこの状態になるのは、エンティティがデータベースから読み取られた場合です。

  • Modified:エンティティのプロパティ値の一部またはすべてが変更されています。 SaveChanges メソッドでは、UPDATE ステートメントが発行されます。

  • Deleted:エンティティには削除のマークが付けられています。 SaveChanges メソッドでは、DELETE ステートメントが発行されます。

  • Detached:エンティティはデータベース コンテキストによって追跡されていません。

デスクトップ アプリにおいて、通常、状態の変更は自動的に設定されます。 エンティティが読み取られ、変更が加えられると、エンティティの状態は自動的に Modified に変更されます。 SaveChanges を呼び出すと、変更されたプロパティのみを更新する SQL UPDATE ステートメントが生成されます。

Web アプリにおいて、エンティティを読み取り、データを表示する DbContext は、ページが表示された後で破棄されます。 ページの OnPostAsync メソッドが呼び出されると、新しい Web 要求が行われ、DbContext の新しいインスタンスが使用されます。 その新しいコンテキスト内のエンティティの再読み取りを行うと、デスクトップの処理がシミュレートされます。

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

このセクションでは、SaveChanges の呼び出しが失敗すると、カスタム エラー メッセージが実装されます。

Pages/Students/Delete.cshtml.cs のコードを次のコードに置き換えます。

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

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

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

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

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

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

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

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

            return Page();
        }

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

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

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

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

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

上記のコードでは次の操作が行われます。

  • ログ記録を追加します。
  • 省略可能なパラメーター saveChangesErrorOnGetAsync メソッド シグネチャに追加します。 saveChangesError は、受講者オブジェクトの削除に失敗した後で、メソッドが呼び出されたかどうかを示します。

一時的なネットワークの問題により、削除操作が失敗する可能性があります。 データベースがクラウド内にある場合は、一時的なネットワーク エラーが発生する可能性が高くなります。 Delete ページの OnGetAsync が UI から呼び出された場合、saveChangesError パラメーターは false です。 削除操作が失敗したために、OnPostAsync によって OnGetAsync が呼び出された場合、saveChangesError パラメーターは true です。

OnPostAsync メソッドは、選択されたエンティティを取得し、Remove メソッドを呼び出して、エンティティの状態を Deleted に設定します。 SaveChanges が呼び出された場合、SQL の DELETE コマンドが生成されます。 Remove が失敗した場合:

  • データベース例外がキャッチされます。
  • [削除] ページの OnGetAsync メソッドが、saveChangesError=true を指定して呼び出されます。

エラー メッセージを Pages/Students/Delete.cshtml に追加します。

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

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

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

アプリを実行し、学生を削除して、Delete ページをテストします。

次の手順

このチュートリアルでは、スキャフォールディング CRUD (作成、読み取り、更新、削除) コードのレビューとカスタマイズを行います。

リポジトリがない

一部の開発者は、サービス レイヤーまたはリポジトリ パターンを使用して、UI (Razor Pages) とデータ アクセス層との間に抽象化レイヤーを作成しています。 このチュートリアルでは、これは行いません。 複雑さを最小限に抑え、チュートリアルの焦点を EF Core に置くために、EF Core コードはページ モデル クラスに直接追加します。

Details ページを更新する

Students ページのスキャフォールディング コードには、登録データが含まれていません。 このセクションでは、Details ページに登録が追加されます。

登録の読み取り

このページに学生の登録データを表示するには、その登録データの読み取りが行われる必要があります。 Pages/Students/Details.cshtml.cs のスキャフォールディング コードでは、Enrollment データを含まない Student データのみが読み取られます。

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

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

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

OnGetAsync メソッドを次のコードに置き換えて、選択した学生の登録データを読み取ります。 変更が強調表示されます。

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

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

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

Include および ThenInclude メソッドにより、コンテキストは Student.Enrollments ナビゲーション プロパティと、各登録内の Enrollment.Course ナビゲーション プロパティを読み込みます。 これらのメソッドは、関連データの読み込みのチュートリアルで詳しく検討します。

AsNoTracking メソッドは、返されたエンティティが現在のコンテキストで更新されないシナリオでパフォーマンスを改善します。 AsNoTracking は、このチュートリアルで後述します。

登録の表示

Pages/Students/Details.cshtml 内のコードを次のコードに置き換えて、登録のリストを表示します。 変更が強調表示されます。

@page
@model ContosoUniversity.Pages.Students.DetailsModel

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

<h1>Details</h1>

<div>
    <h4>Student</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Student.EnrollmentDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Student.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Student.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<div>
    <a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
    <a asp-page="./Index">Back to List</a>
</div>

上記のコードは、Enrollments ナビゲーション プロパティ内でエンティティをループ処理します。 登録ごとに、コースのタイトルとグレードが表示されます。 コース タイトルは、Enrollments エンティティの Course ナビゲーション プロパティに格納されている Course エンティティから取得されます。

アプリを実行し、 [Students] タブを選択し、学生用の [詳細] リンクをクリックします。 選択した受講者のコースとグレードの一覧が表示されます。

1 つのエンティティを読み取る方法

生成されたコードでは、FirstOrDefaultAsync を使用して 1 つのエンティティを読み取ります。 このメソッドでは、何も見つからない場合は null が返されます。それ以外の場合は、クエリのフィルター条件を満たす最初の行が返されます。 FirstOrDefaultAsync は、通常、次の代替手段よりも適しています。

  • SingleOrDefaultAsync - クエリ フィルターを満たす複数のエンティティがある場合に、例外をスローします。 クエリによって複数の行が返される可能性があるかどうかを判断するため、SingleOrDefaultAsync は複数の行をフェッチしようとします。 一意のキーを検索する場合と同様に、クエリが 1 つのエンティティだけを返すことができる場合は、この追加作業は不要です。
  • FindAsync - 主キー (PK) を持つエンティティを検索します。 PK を持つエンティティがコンテキストによって追跡されている場合、データベースに対する要求がなくても該当するエンティティが返されます。 このメソッドは、単一のエンティティを検索するように最適化されていますが、FindAsync を使用して Include を呼び出すことはできません。 したがって、関連データが必要な場合は、FirstOrDefaultAsync を選択することをお勧めします。

ルート データとクエリ文字列

Details ページの URL は https://localhost:<port>/Students/Details?id=1 です。 エンティティの主キー値がクエリ文字列に含まれています。 ルート データのキー値を渡すことを好む開発者もいます。https://localhost:<port>/Students/Details/1 詳細については、「生成されたコードの更新」を参照してください。

Create ページを更新する

Create ページのスキャフォールディングされた OnPostAsync コードは、過剰ポスティングに対して脆弱です。 Pages/Students/Create.cshtml.csOnPostAsync メソッドを次のコードに置き換えます。

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

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

    return Page();
}

TryUpdateModelAsync

上記のコードでは、Student オブジェクトを作成し、ポストされたフォーム フィールドを使用して Student オブジェクトのプロパティを更新します。 TryUpdateModelAsync メソッド:

  • PageModelPageContext プロパティからポストされたフォーム値を使用します。
  • リストされたプロパティのみを更新します (s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate)。
  • "student" のプレフィックスを持つフォーム フィールドを検索します。 たとえば、Student.FirstMidName のようにします。 大文字と小文字の区別はありません。
  • モデル バインド システムを使用して、文字列からフォーム値を Student モデル内の型に変換します。 たとえば、EnrollmentDate は DateTime に変換する必要があります。

アプリを実行し、Create ページをテストする student エンティティを作成します。

過剰ポスティング

ポストされた値を持つフィールドを更新するために TryUpdateModel を使用することは、過剰ポスティングの防止につながり、セキュリティ上のベスト プラクティスとなります。 たとえば、Student エンティティには、この Web ページで更新または追加できない Secret プロパティが含まれています。

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

アプリの作成または更新の Razor ページに Secret フィールドが含まれていない場合でも、ハッカーは過剰ポスティングによって Secret 値を設定することが可能です。 ハッカーは、Fiddler などのツールを使用するか、または何らかの JavaScript を作成して、Secret フォーム値をポストすることが可能です。 元のコードでは、Student インスタンスの作成時にモデル バインダーによって使用されるフィールドを制限していません。

Secret フォーム フィールドに対してハッカーが指定した値はいずれも、データベース内で更新されます。 次の図では、Fiddler ツールを使用して、ポストされたフォームの値に Secret フィールド (値 "OverPost" を含む) が追加されています。

Fiddler adding Secret field

挿入された行の Secret プロパティに値 "OverPost" が正常に追加されています。 これは、アプリ デザイナーで、Create ページで Secret プロパティが設定されることを想定していない場合でも発生します。

ビュー モデル

ビュー モデルは、過剰ポスティングを防ぐもう 1 つの方法を提供します。

アプリケーション モデルは、しばしばドメイン モデルと呼ばれます。 ドメイン モデルには、通常、データベース内の対応するエンティティによって必要とされるすべてのプロパティが含まれています。 ビュー モデルには、使用する UI に必要なプロパティのみが含まれています (たとえば、Create ページ)。

一部のアプリでは、ビュー モデルに加えて、Razor Pages のページ モデル クラスとブラウザーとの間でデータを渡すためにバインド モデルまたは入力モデルも使用します。

次の Student ビュー モデルを考えてみます。

using System;

namespace ContosoUniversity.Models
{
    public class StudentVM
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

次のコードでは、StudentVM ビュー モデルを使用して新しい受講生を作成します。

[BindProperty]
public StudentVM StudentVM { get; set; }

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

    var entry = _context.Add(new Student());
    entry.CurrentValues.SetValues(StudentVM);
    await _context.SaveChangesAsync();
    return RedirectToPage("./Index");
}

SetValues メソッドでは、このオブジェクトの値を設定するために、別の PropertyValues オブジェクトから値を読み取ります。 SetValues では一致するプロパティ名が使用されます。 ビュー モデルの型はモデルの型に関連している必要はなく、プロパティが一致している必要があるだけです。

StudentVM を使用するには、Student ではなく StudentVM を使用するように Create.cshtml を更新する必要があります。

[編集] ページを更新する

Pages/Students/Edit.cshtml.cs で、OnGetAsync メソッドと OnPostAsync メソッドを次のコードに置き換えます。

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

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

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

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

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

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

    return Page();
}

コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。

  • FirstOrDefaultAsyncFindAsync に置き換えられました。 関連データを含める必要がない場合は、FindAsync の方が効率的です。
  • OnPostAsync には id パラメーターがあります。
  • 現在の学生は、空の学生を作成するのではなく、データベースからフェッチされます。

アプリを実行し、学生を作成して編集することでアプリをテストします。

エンティティの状態

データベース コンテキストは、メモリ内のエンティティが、データベース内の対応する行と同期状態にあるかどうかを追跡します。 この追跡情報により、SaveChangesAsync が呼び出されたときに何が起こっているかを特定できます。 たとえば、新しいエンティティが AddAsync メソッドに渡されたとき、そのエンティティの状態は Added に設定されます。 SaveChangesAsync が呼び出されると、データベース コンテキストは SQL の INSERT コマンドを発行します。

エンティティは、次のいずれかの状態になる可能性があります。

  • Added:エンティティはデータベースにまだ存在しません。 SaveChanges メソッドは INSERT ステートメントを発行します。

  • Unchanged:このエンティティでは変更を保存する必要がありません。 エンティティがこの状態になるのは、エンティティがデータベースから読み取られた場合です。

  • Modified:エンティティのプロパティ値の一部またはすべてが変更されています。 SaveChanges メソッドは UPDATE ステートメントを発行します。

  • Deleted:エンティティには削除のマークが付けられています。 SaveChanges メソッドは DELETE ステートメントを発行します。

  • Detached:エンティティはデータベース コンテキストによって追跡されていません。

デスクトップ アプリにおいて、通常、状態の変更は自動的に設定されます。 エンティティが読み取られ、変更が加えられると、エンティティの状態は自動的に Modified に変更されます。 SaveChanges を呼び出すと、変更されたプロパティのみを更新する SQL UPDATE ステートメントが生成されます。

Web アプリにおいて、エンティティを読み取り、データを表示する DbContext は、ページが表示された後で破棄されます。 ページの OnPostAsync メソッドが呼び出されると、新しい Web 要求が行われ、DbContext の新しいインスタンスが使用されます。 その新しいコンテキスト内のエンティティの再読み取りを行うと、デスクトップの処理がシミュレートされます。

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

このセクションでは、SaveChanges の呼び出しが失敗したときにカスタム エラー メッセージを実装します。

Pages/Students/Delete.cshtml.cs 内のコードを次のコードに置き換えます。 変更が強調表示されます (using ステートメントのクリーンアップ以外)。

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

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

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

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

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

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

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

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

            return Page();
        }

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

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

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

            try
            {
                _context.Students.Remove(student);
                await _context.SaveChangesAsync();
                return RedirectToPage("./Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Log the error (uncomment ex variable name and write a log.)
                return RedirectToAction("./Delete",
                                     new { id, saveChangesError = true });
            }
        }
    }
}

上記のコードでは、省略可能なパラメーター saveChangesErrorOnGetAsync メソッド シグネチャに追加します。 saveChangesError は、受講者オブジェクトの削除に失敗した後で、メソッドが呼び出されたかどうかを示します。 一時的なネットワークの問題により、削除操作が失敗する可能性があります。 データベースがクラウド内にある場合は、一時的なネットワーク エラーが発生する可能性が高くなります。 Delete ページの OnGetAsync が UI から呼び出された場合、saveChangesError パラメーターは false です。 OnPostAsync によって OnGetAsync が呼び出された場合 (削除操作が失敗したため)、saveChangesError パラメーターは true です。

OnPostAsync メソッドは、選択されたエンティティを取得し、Remove メソッドを呼び出して、エンティティの状態を Deleted に設定します。 SaveChanges が呼び出されると、SQL DELETE コマンドが生成されます。 Remove が失敗した場合:

  • データベース例外がキャッチされます。
  • [削除] ページの OnGetAsync メソッドが、saveChangesError=true を指定して呼び出されます。

[削除] Razor ページ (Pages/Students/Delete.cshtml) にエラー メッセージを追加します。

@page
@model ContosoUniversity.Pages.Students.DeleteModel

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

<h1>Delete</h1>

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

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

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

アプリを実行し、学生を削除して、Delete ページをテストします。

次の手順