ASP.NET Core Blazor と Entity Framework Core (EF Core)

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

この記事では、サーバー側の アプリで Entity Framework Core () を使用する方法について説明します。

サーバー側 Blazor はステートフル アプリ フレームワークです。 アプリではサーバーへの継続的な接続が維持され、ユーザーの状態はサーバーのメモリ内の「回路」に保持されます。 ユーザー状態の一例として、circuit にスコープされた依存性の注入 (DI) サービス インスタンスに保持されるデータがあります。 Blazor が提供する独自のアプリケーション モデルでは、Entity Framework Core を使用するための特別なアプローチが必要です。

注意

この記事では、サーバー側の Blazor アプリにおける EF Core について説明します。 Blazor WebAssembly アプリは、ほとんどの直接的なデータベース接続を防ぐ WebAssembly サンドボックス内で実行されます。 EF Core での Blazor WebAssembly の実行については、この記事では扱いません。

このガイダンスは、Blazor Web App で対話型サーバー側レンダリング (対話型 SSR) を採用するコンポーネントに適用されます。

このガイダンスは、ホストされている Blazor WebAssembly ソリューションの Server プロジェクトまたは Blazor Server アプリに適用されます。

本番環境のアプリに必要な安全な認証フロー

この記事は、ユーザー認証を必要としないローカル データベースの使用に関連します。 運用アプリでは、使用可能な最も安全な認証フローを使用する必要があります。 デプロイされたテスト環境用および運用環境用の Blazor アプリの認証の詳細については、BlazorSecurity and Identity ノード内の記事を参照してください。

Microsoft Azure サービスの場合は、マネージド ID を使用することをお勧めします。 マネージド ID を使用すると、アプリ コードに資格情報を保存せずに、Azure サービスに対して安全に認証を行うことができます。 詳細については、次のリソースを参照してください。

Blazorムービーデータベースアプリを作成するチュートリアル

データベース操作に を使用するアプリを構築するチュートリアルのエクスペリエンスについては、「 ムービー データベース アプリのビルド (概要)参照してください。 このチュートリアルでは、ムービー データベース内のムービーを表示および管理できる Blazor Web App を作成する方法について説明します。

データベース アクセス

EF Core は、データベース アクセスを構成し、作業単位として機能する手段として、DbContext に依存します。 EF Core は、コンテキストをスコープ サービスとして登録する ASP.NET Core アプリ向けの AddDbContext 拡張機能を提供します。 サーバー側の Blazor アプリでは、インスタンスがユーザーのサーキット内のコンポーネント全体で共有されるため、スコープ サービスの登録が問題になる可能性があります。 DbContext はスレッド セーフではなく、同時に使用するように設計されていません。 次の理由により、既存の有効期間は不適切です。

  • Singleton の場合、アプリのすべてのユーザーで状態が共有され、不適切な同時使用につながります。
  • [範囲指定] (既定値) の場合、同じユーザーのコンポーネント間で同様の問題が発生します。
  • [一時的] の場合、要求ごとに新しいインスタンスが生成されます。ただし、コンポーネントは長寿命になり得るため、意図したよりも長く存続するコンテキストになります。

以下の推奨事項は、サーバー側の Blazor アプリで EF Core を使用する際に一貫したアプローチを提供するように設計されています。

  • 操作ごとに 1 つのコンテキストを使用することを検討してください。 コンテキストは、高速でオーバーヘッドの少ないインスタンス化を目的として設計されています。

    using var context = new ProductsDatabaseContext();
    
    return await context.Products.ToListAsync();
    
  • フラグを使用して、複数の操作が同時に実行されるのを防止します。

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Loading = true; ブロックの try 行の後にデータベース操作を配置します。

    スレッドセーフ性は問題にならないため、読み込みロジックではデータベースレコードをロックする必要はありません。 読み込みロジックは、データのフェッチ中にユーザーが誤ってボタンを選択したり、フィールドを更新したりしないように、UI コントロールを無効にするために使用されます。

  • 複数のスレッドが同じコード ブロックにアクセスする可能性がある場合は、ファクトリを注入し、操作ごとに新しいインスタンスを作成します。 それ以外の場合は、通常、コンテキストを注入して使用するだけで十分です。

  • EF Core の変更追跡または同時実行制御を利用する、より長期間の操作の場合は、コンポーネントの有効期間にコンテキストの範囲を限定します。

新しい DbContext インスタンス

新しい DbContext インスタンスを作成する最も速い方法は、new を使用して新しいインスタンスを作成することです。 ただし、追加の依存関係を解決する必要がある次のようなシナリオがあります。

警告

アプリ シークレット、接続文字列、資格情報、パスワード、個人識別番号 (PIN)、プライベート C#/.NET コード、秘密キー/トークンをクライアント側コードに格納しないでください。これは常に安全ではありません。 テスト/ステージング環境と運用環境では、サーバー側の Blazor コードと Web API は、資格情報をプロジェクト コードまたは構成ファイル内に保持しない、セキュリティで保護された認証フローを使用する必要があります。 ローカル開発テスト以外では、環境変数が最も安全なアプローチではないため、環境変数を使用して機密データを格納しないようにすることをお勧めします。 ローカル開発テストでは、機密データをセキュリティで保護するために、 Secret Manager ツール をお勧めします。 詳細については、「 機密データと資格情報を安全に管理するを参照してください。

依存関係を持つ新しい DbContext を作成するには、ファクトリを使用することをお勧めします。 EF Core .NET 5 以降では、新しいコンテキストを作成するための組み込みファクトリが提供されます。

.NET 5 より前のバージョンの .NET では、次の DbContextFactoryを使用します。

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);
    }
}

前のファクトリでは:

次の例では、SQLite 構成し、連絡先を管理するアプリでデータ ログを有効にします。 このコードでは、拡張メソッド (AddDbContextFactory) を使用して、依存関係挿入用にデータベース ファクトリを構成し、既定のオプションを指定しています。

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"));

ファクトリがコンポーネントに挿入され、新しい DbContext インスタンスが作成されます。

データベース コンテキストのスコープをコンポーネント メソッドに設定する

ファクトリはコンポーネントに挿入されます。

@inject IDbContextFactory<ContactContext> DbFactory

ファクトリ(DbContext)を使用して、メソッドに対するDbFactoryを作成します。

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    if (context.Contacts is not null)
    {
        var contact = await context.Contacts.FirstAsync(...);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }
}

データベース コンテキストのスコープをコンポーネントの有効期間に設定する

コンポーネントの有効期間中は存在する DbContext を作成するとよいでしょう。 これにより、それを作業単位として使用し、変更の追跡や同時実行の解決などの組み込み機能を利用することができます。

IDisposable を実装し、コンポーネントにファクトリを挿入します。

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

DbContextのプロパティを設定します。

private ContactContext? Context { get; set; }

OnInitializedAsync がオーバーライドされ、DbContextが作成されます。

protected override async Task OnInitializedAsync()
{
    Context = DbFactory.CreateDbContext();
}

コンポーネントが破棄されると、DbContext が破棄されます。

public void Dispose() => Context?.Dispose();

機密データのログ記録を有効にする

EnableSensitiveDataLogging では、例外メッセージおよびフレームワークのログにアプリケーション データが含まれます。 ログに記録されるデータには、エンティティ インスタンスのプロパティに割り当てられた値と、データベースに送信されたコマンドのパラメーター値を含めることができます。 EnableSensitiveDataLogging を使用したデータのログ記録はセキュリティ リスクです。データベースに対して実行された SQL ステートメントをログに記録するときに、パスワードやその他の個人を特定できる情報 (PII) が公開される可能性があるためです。

ローカルでの開発とテストの EnableSensitiveDataLogging のみを有効にすることをお勧めします。

if (builder.Environment.IsDevelopment())
{
    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"));
}