ASP.NET Core Blazor Server と Entity Framework Core (EFCore)

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

注意

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

サンプル アプリ

このサンプル アプリは、EF Core を使用する Blazor Server アプリのリファレンスとして作成されました。 サンプル アプリには、並べ替えとフィルター処理、削除、追加、更新の各操作を行うグリッドが含まれています。 このサンプルは、EF Core を使用してオプティミスティック同時実行制御を処理する方法を示しています。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

サンプルにはローカルの SQLite データベースが使用されているため、どのプラットフォームでも使用できます。 このサンプルでは、生成された SQL クエリを表示するようにデータベースのログも構成されています。 これは appsettings.Development.json で構成されています。

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

グリッド、追加、および表示の各コンポーネントでは、操作ごとにコンテキストが作成される "操作ごとのコンテキスト" パターンが使用されます。 編集コンポーネントでは、コンポーネントごとにコンテキストが作成される "コンポーネントごとのコンテキスト" パターンが使用されます。

注意

このトピックのコード例には、表示されていない名前空間とサービスを必要とするものもあります。 Razor の例に必要な @using および @inject ディレクティブを含む完全に機能するコードを検査するには、サンプル アプリを参照してください。

データベース アクセス

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

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

以下の推奨事項は、Blazor Server アプリで 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; 行の後に操作を配置します。

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

新しい DbContext インスタンス

新しい DbContext インスタンスを作成する最も簡単な方法は、new を使用して新しいインスタンスを作成することです。 ただし、追加の依存関係を解決する必要があるシナリオがいくつかあります。 たとえば、DbContextOptions を使用してコンテキストを構成することができます。

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

次の例では、SQLite を構成し、データのログ記録を有効にします。 このコードでは、拡張メソッド (AddDbContextFactory) を使用して、DI のデータベース ファクトリを構成し、既定のオプションを指定しています。

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

ファクトリはコンポーネントに挿入され、新しいインスタンスを作成するために使用されます。 たとえば、Pages/Index.razor では次のようにします。

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

注意

Wrapper は、GridWrapper コンポーネントへのコンポーネント参照です。 サンプル アプリIndex コンポーネント (Pages/Index.razor) を参照してください。

新しい DbContext インスタンスは、[ASP.NET Core の Identity モデル] を使用する場合など、DbContext ごとに接続文字列を構成できるファクトリを使用して作成できます。

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

コンポーネントの有効期間の範囲

コンポーネントの有効期間中は存在する DbContext を作成することができます。 これにより、それを作業単位として使用し、変更の追跡や同時実行の解決などの組み込み機能を利用することができます。 ファクトリーを使用してコンテキストを作成し、コンポーネントの有効期間中はそれを追跡できます。 まず IDisposable を実装し、Pages/EditContact.razor に示すようにファクトリを挿入します。

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

サンプル アプリでは、コンポーネントが破棄されるときに、コンテキストも確実に破棄されます。

public void Dispose()
{
    Context.Dispose();
}

最後に、OnInitializedAsync は新しいコンテキストを作成するためにオーバーライドされます。 サンプル アプリでは、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();
}

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

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

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

注意

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

サンプル アプリ

このサンプル アプリは、EF Core を使用する Blazor Server アプリのリファレンスとして作成されました。 サンプル アプリには、並べ替えとフィルター処理、削除、追加、更新の各操作を行うグリッドが含まれています。 このサンプルは、EF Core を使用してオプティミスティック同時実行制御を処理する方法を示しています。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

サンプルにはローカルの SQLite データベースが使用されているため、どのプラットフォームでも使用できます。 このサンプルでは、生成された SQL クエリを表示するようにデータベースのログも構成されています。 これは appsettings.Development.json で構成されています。

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

グリッド、追加、および表示の各コンポーネントでは、操作ごとにコンテキストが作成される "操作ごとのコンテキスト" パターンが使用されます。 編集コンポーネントでは、コンポーネントごとにコンテキストが作成される "コンポーネントごとのコンテキスト" パターンが使用されます。

注意

このトピックのコード例には、表示されていない名前空間とサービスを必要とするものもあります。 Razor の例に必要な @using および @inject ディレクティブを含む完全に機能するコードを検査するには、サンプル アプリを参照してください。

データベース アクセス

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

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

以下の推奨事項は、Blazor Server アプリで 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; 行の後に操作を配置します。

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

新しい DbContext インスタンス

新しい DbContext インスタンスを作成する最も簡単な方法は、new を使用して新しいインスタンスを作成することです。 ただし、追加の依存関係を解決する必要があるシナリオがいくつかあります。 たとえば、DbContextOptions を使用してコンテキストを構成することができます。

依存関係を持つ新しい DbContext を作成するには、ファクトリを使用することをお勧めします。 このサンプル アプリでは、Data/DbContextFactory.cs で独自のファクトリを実装しています。

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

        public TContext CreateDbContext()
        {
            if (provider == null)
            {
                throw new InvalidOperationException(
                    $"You must configure an instance of IServiceProvider");
            }

            return ActivatorUtilities.CreateInstance<TContext>(provider);
        }
    }
}

前のファクトリは、次のようになっています。

次の例では、SQLite を構成し、データのログ記録を有効にします。 このコードでは、拡張メソッドを使用して、DI のデータベース ファクトリを構成し、既定のオプションを指定しています。

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

ファクトリはコンポーネントに挿入され、新しいインスタンスを作成するために使用されます。 たとえば、Pages/Index.razor では次のようにします。

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

注意

Wrapper は、GridWrapper コンポーネントへのコンポーネント参照です。 サンプル アプリIndex コンポーネント (Pages/Index.razor) を参照してください。

新しい DbContext インスタンスは、[ASP.NET Core の Identity モデル])(xref:security/authentication/customize_identity_model) を使用する場合など、DbContext ごとに接続文字列を構成できるファクトリを使用して作成できます。

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

コンポーネントの有効期間の範囲

コンポーネントの有効期間中は存在する DbContext を作成することができます。 これにより、それを作業単位として使用し、変更の追跡や同時実行の解決などの組み込み機能を利用することができます。 ファクトリーを使用してコンテキストを作成し、コンポーネントの有効期間中はそれを追跡できます。 まず IDisposable を実装し、Pages/EditContact.razor に示すようにファクトリを挿入します。

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

サンプル アプリでは、コンポーネントが破棄されるときに、コンテキストも確実に破棄されます。

public void Dispose()
{
    Context.Dispose();
}

最後に、OnInitializedAsync は新しいコンテキストを作成するためにオーバーライドされます。 サンプル アプリでは、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();
}

前の例の場合:

  • Busytrue に設定すると、非同期操作が開始されることがあります。 Busyfalse に戻すと、非同期操作は完了するはずです。
  • 追加のエラー処理ロジックを 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

その他のリソース