ASP.NET Core Blazor と Entity Framework Core (EF Core)
注意
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
この記事では、サーバー側の Blazor アプリで Entity Framework Core (EF Core) を使用する方法について説明します。
サーバー側 Blazor はステートフル アプリ フレームワークです。 アプリではサーバーへの継続的な接続が維持され、ユーザーの状態は "回線" 内のサーバーのメモリに保持されます。 ユーザー状態の一例として、回線に範囲が設定されている依存関係の挿入 (DI) サービス インスタンスに保存されているデータがあります。 Blazor が提供する独自のアプリケーション モデルでは、Entity Framework Core を使用するための特別なアプローチが必要です。
Note
この記事では、サーバー側の Blazor アプリでの EF Core について説明します。 Blazor WebAssembly アプリは、ほとんどの直接データベース接続が防止される WebAssembly サンドボックス内で実行されます。 Blazor WebAssembly での EF Core の実行については、この記事では扱いません。
このガイダンスは、Blazor Web App で対話型サーバー側レンダリング (対話型 SSR) を採用するコンポーネントに適用されます。
このガイダンスは、ホストされている Blazor WebAssembly ソリューションまたは Blazor Server アプリの Server
プロジェクトに適用されます。
運用アプリに必要な安全な認証フロー
この記事では、ユーザー認証が不要なローカル データベースを使用します。 運用アプリでは、使用可能な最も安全な認証フローを使用する必要があります。 デプロイされたテストおよび運用 Blazor アプリの認証の詳細については、Blazorセキュリティと Identity ノードの記事をご覧ください。
Microsoft Azure サービスの場合は、マネージド ID を使用することをお勧めします。 マネージド ID を使用すると、アプリ コードに資格情報を保存せずに、Azure サービスに対して安全に認証を行うことができます。 詳細については、次のリソースを参照してください。
- Azure リソース用マネージド ID とは (Microsoft Entra のドキュメント)
- Azure サービスのドキュメント
サンプル アプリ
このサンプル アプリは、EF Core を使用するサーバー側の Blazor アプリのリファレンスとして作成されました。 サンプル アプリには、並べ替えとフィルター処理、削除、追加、更新の各操作を行うグリッドが含まれています。
このサンプルは、EF Core を使用してオプティミスティック同時実行制御を処理する方法を示しています。 ただし、 サンプル アプリのデータベース プロバイダーである SQLite データベースでは、ネイティブデータベース生成コンカレンシー トークン はサポートされていません。 サンプル アプリでのコンカレンシーを示すには、データベース生成のコンカレンシー トークン (SQL Server プロバイダーなど) をサポートする別のデータベース プロバイダーを 採用します。
サンプル コードの表示またはダウンロード (ダウンロード方法): 採用する .NET のバージョンに一致するフォルダーを選びます。 そのバージョン フォルダー内で、BlazorWebAppEFCore
という名前のサンプルにアクセスします。
サンプル コードの表示またはダウンロード (ダウンロード方法): 採用する .NET のバージョンに一致するフォルダーを選びます。 そのバージョン フォルダー内で、BlazorServerEFCoreSample
という名前のサンプルにアクセスします。
サンプルにはローカルの SQLite データベースが使用されているため、どのプラットフォームでも使用できます。 このサンプルでは、生成された SQL クエリを表示するようにデータベースのログも構成されています。 これは appsettings.Development.json
で構成されています。
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
グリッド、追加、および表示の各コンポーネントでは、操作ごとにコンテキストが作成される "操作ごとのコンテキスト" パターンが使用されます。 編集コンポーネントでは、コンポーネントごとにコンテキストが作成される "コンポーネントごとのコンテキスト" パターンが使用されます。
Note
このトピックのコード例には、表示されていない名前空間とサービスを必要とするものもあります。 Razor の例に必要な @using
および @inject
ディレクティブを含む完全に機能するコードを検査するには、サンプル アプリを参照してください。
ムービー データベース アプリの作成に関する Blazor チュートリアル
データベースの操作に使用するアプリの構築に関EF Coreするチュートリアル エクスペリエンスについては、「ムービー データベース アプリの構築 Blazor (概要)」を参照してください。 このチュートリアルでは、ムービー データベース内のムービーを Blazor Web App 表示および管理できるファイルを作成する方法について説明します。
データベース アクセス
EF Core では、データベース アクセスを構成し、"作業単位" として機能する手段として DbContext を利用しています。 EF Core には、コンテキストをスコープ サービスとして登録する ASP.NET Core アプリの AddDbContext 拡張機能が用意されています。 サーバー側の Blazor アプリでは、インスタンスがユーザーの回線内のコンポーネント全体で共有されるため、スコープ サービスの登録が問題になる可能性があります。 DbContext はスレッド セーフではなく、同時に使用するように設計されていません。 次の理由により、既存の有効期間は不適切です。
- [Singleton](シングルトン) の場合、アプリのすべてのユーザーで状態が共有され、不適切な同時使用につながります。
- [範囲指定] (既定値) の場合、同じユーザーのコンポーネント間で同様の問題が発生します。
- [一時的] の場合、要求ごとに新しいインスタンスが生成されます。ただし、コンポーネントの有効期間が長くなる可能性があるため、意図したよりも時間のかかるコンテキストになります。
以下の推奨事項は、サーバー側の Blazor アプリで EF Core を使用する上で一貫したアプローチを提供するように設計されています。
操作ごとに 1 つのコンテキストを使用することを検討してください。 コンテキストは、高速でオーバーヘッドの少ないインスタンス化を目的として設計されています。
using var context = new MyContext(); return await context.MyEntities.ToListAsync();
フラグを使用して、複数の同時操作を防止します。
if (Loading) { return; } try { Loading = true; ... } finally { Loading = false; }
try
ブロックのLoading = true;
行の後に操作を配置します。スレッド セーフは問題にならないため、読み込みロジックにデータベース レコードのロックは必要ありません。 読み込みロジックは、データのフェッチ中にユーザーが誤ってボタンを選択したり、フィールドを更新したりしないように、UI コントロールを無効にするために使用されます。
複数のスレッドが同じコード ブロックにアクセスする可能性がある場合は、ファクトリを挿入 し、操作ごとに新しいインスタンスを作成します。 それ以外の場合は、通常、コンテキストを挿入して使用するだけで十分です。
EF Core の変更追跡または同時実行制御を利用する、より長期間の操作の場合は、コンポーネントの有効期間にコンテキストの範囲を限定します。
新しい DbContext
インスタンス
新しい DbContext インスタンスを作成する最も簡単な方法は、new
を使用して新しいインスタンスを作成することです。 ただし、追加の依存関係を解決する必要がある次のようなシナリオがあります。
DbContextOptions
を使用してコンテキストを構成する。- DbContext ごとに接続文字列を使用する (たとえば、ASP.NET Core の Identity モデルを使用する場合など)。 詳細については、マルチテナントに関するページ (EF Core ドキュメント) を参照してください。
警告
アプリ シークレット、接続文字列、資格情報、パスワード、個人識別番号 (PIN)、プライベート C#/.NET コード、秘密キー/トークンをクライアント側コードに格納しないでください。これは常に安全ではありません。 テスト/ステージング環境と運用環境では、サーバー側 Blazor コードと Web API は、プロジェクト コードまたは構成ファイル内で資格情報を維持しないように、セキュリティで保護された認証フローを使用する必要があります。 ローカル開発テスト以外では、環境変数が最も安全なアプローチではないため、環境変数を使用して機密データを格納しないようにすることをお勧めします。 ローカル開発テストでは、機密データを セキュリティで保護するために Secret Manager ツール を使用することをお勧めします。 詳細については、「機密データと資格情報を安全に管理する」を参照してください。
依存関係を持つ新しい DbContext を作成するには、ファクトリを使用することをお勧めします。 EF Core 5.0 以降には、新しいコンテキストを作成するためのファクトリが組み込まれています。
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BlazorServerDbContextExample.Data
{
public class DbContextFactory<TContext>
: IDbContextFactory<TContext> where TContext : DbContext
{
private readonly IServiceProvider provider;
public DbContextFactory(IServiceProvider provider)
{
this.provider = provider ?? throw new ArgumentNullException(
$"{nameof(provider)}: You must configure an instance of " +
"IServiceProvider");
}
public TContext CreateDbContext() =>
ActivatorUtilities.CreateInstance<TContext>(provider);
}
}
前のファクトリは、次のようになっています。
- すべての依存関係は、サービス プロバイダーを介して ActivatorUtilities.CreateInstance によって満たされています。
IDbContextFactory
は、EF Core ASP.NET Core 5.0 以降で使用できます。そのため、インターフェイスは、ASP.NET Core 3.x 用のサンプル アプリに実装されます。
次の例では、SQLite を構成し、データのログ記録を有効にします。 このコードでは、拡張メソッド (AddDbContextFactory
) を使用して、DI のデータベース ファクトリを構成し、既定のオプションを指定しています。
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
ファクトリはコンポーネントに挿入され、新しい DbContext
インスタンスを作成するために使用されます。
サンプル アプリの home ページでは、IDbContextFactory<ContactContext>
がコンポーネントに挿入されます。
@inject IDbContextFactory<ContactContext> DbFactory
DeleteContactAsync
メソッドの連絡先を削除するには、ファクトリ (DbFactory
) を使用して DbContext
が作成されます。
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
var contact = await context.Contacts.FirstAsync(
c => c.Id == Wrapper.DeleteRequestId);
if (contact != null)
{
context.Contacts.Remove(contact);
await context.SaveChangesAsync();
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
var contact = await context.Contacts.FirstAsync(
c => c.Id == Wrapper.DeleteRequestId);
if (contact != null)
{
context.Contacts.Remove(contact);
await context.SaveChangesAsync();
}
Filters.Loading = false;
await ReloadAsync();
}
注意
Filters
は挿入された IContactFilters
であり、Wrapper
は GridWrapper
コンポーネントに対するコンポーネント参照です。 サンプル アプリの Home
コンポーネント (Components/Pages/Home.razor
) を参照してください。
Note
Filters
は挿入された IContactFilters
であり、Wrapper
は GridWrapper
コンポーネントに対するコンポーネント参照です。 サンプル アプリの Index
コンポーネント (Pages/Index.razor
) を参照してください。
コンポーネントの有効期間の範囲
コンポーネントの有効期間中は存在する DbContext を作成することができます。 これにより、それを作業単位として使用し、変更の追跡や同時実行の解決などの組み込み機能を利用することができます。
ファクトリーを使用してコンテキストを作成し、コンポーネントの有効期間中はそれを追跡できます。 まず IDisposable を実装し、EditContact
コンポーネント (Components/Pages/EditContact.razor
) に示すようにファクトリを挿入します。
ファクトリーを使用してコンテキストを作成し、コンポーネントの有効期間中はそれを追跡できます。 まず IDisposable を実装し、EditContact
コンポーネント (Pages/EditContact.razor
) に示すようにファクトリを挿入します。
@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory
サンプル アプリでは、コンポーネントが破棄されるときに、コンテキストも確実に破棄されます。
public void Dispose() => Context?.Dispose();
public void Dispose() => Context?.Dispose();
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
最後に、OnInitializedAsync
は新しいコンテキストを作成するためにオーバーライドされます。 サンプル アプリでは、OnInitializedAsync
を使用して同じメソッドで連絡先を読み込んでいます。
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
Contact = await Context.Contacts
.SingleOrDefaultAsync(c => c.Id == ContactId);
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
Contact = await Context.Contacts
.SingleOrDefaultAsync(c => c.Id == ContactId);
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
前の例の場合:
Busy
をtrue
に設定すると、非同期操作が開始されることがあります。Busy
をfalse
に戻すと、非同期操作は完了するはずです。- 追加のエラー処理ロジックを
catch
ブロックに配置します。
機密データのログ記録を有効にする
EnableSensitiveDataLogging では、例外メッセージおよびフレームワークのログにアプリケーション データが含まれます。 ログに記録されるデータには、エンティティ インスタンスのプロパティに割り当てられた値と、データベースに送信されたコマンドのパラメーター値を含めることができます。 EnableSensitiveDataLogging を使用したデータのログ記録はセキュリティ リスクです。データベースに対して実行された SQL ステートメントをログに記録するときに、パスワードやその他の個人を特定できる情報 (PII) が公開される可能性があるためです。
EnableSensitiveDataLogging は、開発とテストのためにのみ有効にすることをお勧めします。
#if DEBUG
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
.EnableSensitiveDataLogging());
#else
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif
その他のリソース
ASP.NET Core