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

注意

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

この記事では、サーバー側の 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 アプリで対話型サーバー側レンダリング (対話型 SSR) を採用するコンポーネントに適用されます。

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

サンプル アプリ

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

サンプル コードの表示またはダウンロード (ダウンロード方法): 採用する .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": "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 ディレクティブを含む完全に機能するコードを検査するには、サンプル アプリを参照してください。

データベース アクセス

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 を使用して新しいインスタンスを作成することです。 ただし、追加の依存関係を解決する必要がある次のようなシナリオがあります。

依存関係を持つ新しい 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);
    }
}

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

次の例では、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"));
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 インスタンスを作成するために使用されます。

サンプル アプリのホーム ページでは、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;

    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 であり、WrapperGridWrapper コンポーネントに対するコンポーネント参照です。 サンプル アプリの Home コンポーネント (Components/Pages/Home.razor) を参照してください。

Note

Filters は挿入された IContactFilters であり、WrapperGridWrapper コンポーネントに対するコンポーネント参照です。 サンプル アプリの 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();
}

最後に、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;
    }

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

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

前の例の場合:

  • 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

その他のリソース