パート 2、ASP.NET Core での EF Core を使用した Razor ページ - CRUD
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
作成者: Tom Dykstra、Jeremy Likness、Jon 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.cs
の OnPostAsync
メソッドを次のコードに置き換えます。
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 メソッド:
- PageModel の PageContext プロパティからポストされたフォーム値を使用します。
- リストされたプロパティのみを更新します (
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
フォーム フィールドに対してハッカーが指定した値はいずれも、データベース内で更新されます。 次の図は、ポストされたフォームの値に、値 "OverPost" が含まれる Secret
フィールドを追加している Fiddler ツールを示しています。
挿入された行の 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();
}
コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。
FirstOrDefaultAsync
は FindAsync に置き換えられました。 関連データを含める必要がない場合は、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 });
}
}
}
}
上記のコードでは次の操作が行われます。
- ログ記録を追加します。
- 省略可能なパラメーター
saveChangesError
をOnGetAsync
メソッド シグネチャに追加します。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.cs
の OnPostAsync
メソッドを次のコードに置き換えます。
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 メソッド:
- PageModel の PageContext プロパティからポストされたフォーム値を使用します。
- リストされたプロパティのみを更新します (
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
フォーム フィールドに対してハッカーが指定した値はいずれも、データベース内で更新されます。 次の図は、ポストされたフォームの値に、値 "OverPost" が含まれる Secret
フィールドを追加している Fiddler ツールを示しています。
挿入された行の 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();
}
コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。
FirstOrDefaultAsync
は FindAsync に置き換えられました。 関連データを含める必要がない場合は、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 });
}
}
}
}
上記のコードでは次の操作が行われます。
- ログ記録を追加します。
- 省略可能なパラメーター
saveChangesError
をOnGetAsync
メソッド シグネチャに追加します。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.cs
の OnPostAsync
メソッドを次のコードに置き換えます。
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 メソッド:
- PageModel の PageContext プロパティからポストされたフォーム値を使用します。
- リストされたプロパティのみを更新します (
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" を含む) が追加されています。
挿入された行の 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();
}
コードの変更は [作成] ページに似ています。ただし、次のようないくつかの例外があります。
FirstOrDefaultAsync
は FindAsync に置き換えられました。 関連データを含める必要がない場合は、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 });
}
}
}
}
上記のコードでは、省略可能なパラメーター saveChangesError
を OnGetAsync
メソッド シグネチャに追加します。 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 ページをテストします。
次の手順
ASP.NET Core