ASP.NET Core Blazor 依存関係の挿入

注意

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

警告

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

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

作成者: Rainer StropekMike Rousos

この記事では、Blazor アプリでサービスをコンポーネントに挿入する方法について説明します。

依存関係の挿入 (DI) は、中央の場所で構成されたサービスにアクセスするための手法です。

  • フレームワークによって登録されたサービスは、Razor コンポーネントに直接挿入できます。
  • Blazor アプリによって、カスタム サービスの定義と登録が行われ、DI を通じてアプリ全体でそれらが使用できるようになります。

Note

このトピックを読む前に、ASP.NET Core での依存関係の挿入に関する記事をお読みになることをお勧めします。

既定のサービス

Blazor アプリでよく使用されるサービスを次の表に示します。

サービス 有効期間 説明
HttpClient スコープ

URI によって識別されるリソースに HTTP 要求を送信し、そのリソースから HTTP 応答を受信するためのメソッドが提供されます。

クライアント側では、HttpClient のインスタンスは Program ファイル内のアプリによって登録され、バックグラウンドで HTTP トラフィックの処理にブラウザーを使います。

サーバー側では、既定では HttpClient はサービスとして構成されていません。 サーバー側のコードで、HttpClient を指定します。

詳しくは、「ASP.NET Core Blazor アプリから Web API を呼び出す」をご覧ください。

HttpClient は、シングルトンではなく、スコープ サービスとして登録されます。 詳細については、「サービスの有効期間」セクションを参照してください。

IJSRuntime

クライアント側: シングルトン

サーバー側: スコープ済み

Blazor フレームワークによって、アプリのサービス コンテナーに IJSRuntime が登録されます。

JavaScript の呼び出しがディスパッチされる JavaScript ランタイムのインスタンスを表します。 詳細については、「ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す」を参照してください。

シングルトン サービスにサービスを挿入する場合は、次のいずれかの方法を使用します:

  • サービス登録のスコープを IJSRuntime の登録と一致するように変更します。これはサービスでユーザー固有の状態を処理する場合に適切です。
  • IJSRuntime をシングルトン サービスの実装に、シングルトンに挿入するのではなく、そのメソッド呼び出しの引数として渡します。
NavigationManager

クライアント側: シングルトン

サーバー側: スコープ済み

Blazor フレームワークによって、アプリのサービス コンテナーに NavigationManager が登録されます。

URI とナビゲーション状態を操作するためのヘルパーが含まれます。 詳細については、「URI およびナビゲーション状態ヘルパー」を参照してください。

Blazor フレームワークによって登録された追加のサービスは、ドキュメントで説明されており、構成やログ記録などの Blazor 機能の説明に使用されています。

カスタム サービス プロバイダーでは、表に示されている既定のサービスは自動的に提供されません。 カスタム サービス プロバイダーを使用し、表に示されているいずれかのサービスが必要な場合は、必要なサービスを新しいサービス プロバイダーに追加します。

クライアント側サービスの追加

Program ファイルで、アプリのサービス コレクション用のサービスを構成します。 次の例では、ExampleDependency の実装が IExampleDependency に登録されます。

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
...

await builder.Build().RunAsync();

ホストが構築されると、コンポーネントがレンダリングされる前に、ルート DI スコープからサービスを使用できるようになります。 これは、コンテンツをレンダリングする前に初期化ロジックを実行する場合に役に立ちます。

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync();

await host.RunAsync();

ホストによって、アプリの中央構成インスタンスが提供されます。 前の例を基にして、天気予報サービスの URL を、既定の構成ソース (appsettings.json など) から InitializeWeatherAsync に渡します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);
...
builder.Services.AddSingleton<WeatherService>();
...

var host = builder.Build();

var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeWeatherAsync(
    host.Configuration["WeatherServiceUrl"]);

await host.RunAsync();

サーバー側サービスの追加

新しいアプリを作成した後、Program ファイルの一部を調べます。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

builder 変数は、サービス記述子オブジェクトのリストである IServiceCollection を持つ WebApplicationBuilder を表します。 サービスは、サービス コレクションにサービス記述子を提供することによって追加されます。 次の例では、IDataAccess インターフェイスとその具象実装 DataAccess での概念を示します。

builder.Services.AddSingleton<IDataAccess, DataAccess>();

新しいアプリを作成した後、Startup.csStartup.ConfigureServices メソッドを調べます。

using Microsoft.Extensions.DependencyInjection;

...

public void ConfigureServices(IServiceCollection services)
{
    ...
}

ConfigureServices メソッドには、サービス記述子オブジェクトのリストである IServiceCollection が渡されます。 サービスは、ConfigureServices メソッドでサービス コレクションにサービス記述子を提供することによって追加されます。 次の例では、IDataAccess インターフェイスとその具象実装 DataAccess での概念を示します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IDataAccess, DataAccess>();
}

一般的なサービスの登録

1 つ以上の一般的なサービスがクライアント側とサーバー側で必要な場合は、クライアント側のメソッドに共通サービス登録を配置し、メソッドを呼び出して両方のプロジェクトにサービスを登録できます。

まず、一般的なサービス登録を別のメソッドに組み込みます。 たとえば、ConfigureCommonServices メソッドをクライアント側に作成します:

public static void ConfigureCommonServices(IServiceCollection services)
{
    services.Add...;
}

クライアント側 Program ファイルの場合は、共通サービスを登録するために ConfigureCommonServices を呼び出します:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

サーバー側 Program ファイルで、共通サービスを登録する ConfigureCommonServices 呼び出します:

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

この方法の例については、「ASP.NET Core Blazor WebAssembly のセキュリティに関するその他のシナリオ」を参照してください。

プリレンダリング中にクライアント側サービスが失敗する

このセクションは、Blazor Web App の WebAssembly コンポーネントにのみ適用されます。

通常、Blazor Web App はクライアント側の WebAssembly コンポーネントをプリレンダリングします。 .Client プロジェクトに登録されている必要なサービスのみでアプリが実行されている場合、コンポーネントがプリレンダリング中にその必要なサービスを使用しようとすると、アプリの実行で次のようなランタイム エラーが発生します。

InvalidOperationException: 型 '{ASSEMBLY}}.Client.Pages.{COMPONENT NAME}' の {PROPERTY} の値を指定できません。 型 '{SERVICE}' の登録済みサービスはありません。

この問題を解決するには、次の "いずれか" の方法を使用します。

  • メイン プロジェクトにサービスを登録して、コンポーネントのプリレンダリング中に使用できるようにする。
  • コンポーネントにプリレンダリングが必要ない場合は、ASP.NET Core Blazor レンダー モードのガイダンスに従ってプリレンダリングを無効にする。 このアプローチを採用する場合、サービスをメイン プロジェクトに登録する必要はありません。

詳細については、「プリレンダリング中にクライアント側サービスによる解決が失敗する」を参照してください。

サービスの有効期間

サービスは、次の表に示す有効期間で構成できます。

有効期間 説明
Scoped

クライアント側には現在、DI スコープの概念はありません。 Scoped 登録済みサービスは Singleton サービスのように動作します。

サーバー側の開発では、HTTP 要求間で Scoped 有効期間がサポートされていますが、クライアント側に読み込まれたコンポーネント間の SignalR 接続/回線メッセージ間ではサポートされていません。 アプリの Razor ページまたは MVC の部分では、スコープ付きサービスが通常どおりに処理され、ページまたはビュー間を移動するとき、またはページやビューからコンポーネントに移動するときに、"各 HTTP 要求" に対してサービスが再作成されます。 クライアント上のコンポーネント間を移動するときは、スコープ付きサービスは再構築されません。この場合、サーバーとの通信は、HTTP 要求ではなく、ユーザーの回線の SignalR 接続を介して行われます。 次のクライアント上のコンポーネント シナリオでは、ユーザー用に新しい回線が作成されるため、スコープ付きサービスは再構築されます。

  • ユーザーがブラウザーのウィンドウを閉じる場合。 ユーザーは新しいウィンドウを開き、アプリに戻ります。
  • ユーザーが、ブラウザー ウィンドウでアプリのタブを閉じる場合。 ユーザーは新しいタブを開き、アプリに戻ります。
  • ユーザーが、ブラウザーの再読み込みまたは更新ボタンを選択する場合。

サーバー側アプリでのユーザー状態の保持の詳細については、「ASP.NET Core Blazor 状態管理」を参照してください。

Singleton DI では、サービスの "単一インスタンス" が作成されます。 Singleton サービスを必要とするすべてのコンポーネントは、サービスの同じインスタンスを受け取ります。
Transient コンポーネントは、サービス コンテナーから Transient サービスのインスタンスを取得するたびに、サービスの "新しいインスタンス" を受け取ります。

DI システムは、ASP.NET Core の DI システムが基になっています。 詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。

コンポーネント内のサービスを要求する

コンポーネントにサービスを挿入するために、Blazor はコンストラクター挿入プロパティ挿入をサポートしています。

コンストラクターの挿入

サービスがサービス コレクションに追加されたら、コンストラクター挿入を使って 1 つ以上のサービスをコンポーネントに挿入します。 次の例では、NavigationManager サービスを挿入します。

ConstructorInjection.razor:

@page "/constructor-injection"

<button @onclick="HandleClick">
    Take me to the Counter component
</button>

ConstructorInjection.razor.cs:

using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

プロパティの挿入

サービスがサービス コレクションに追加されたら、@injectRazor ディレクティブ (2 つのパラメーターがあります) を使って、1 つ以上のサービスをコンポーネントに挿入します。

  • 型:挿入するサービスの型。
  • プロパティ:挿入されたアプリ サービスを受け取るプロパティの名前。 プロパティを手動で作成する必要はありません。 プロパティはコンパイラによって作成されます。

詳細については、「ASP.NET Core でのビューへの依存関係の挿入」を参照してください。

異なるサービスを挿入するには、複数の @inject ステートメントを使用します。

次の例は、@inject ディレクティブの使用方法を示しています。 Services.NavigationManager を実装するサービスを、コンポーネントのプロパティ Navigation に挿入します。 このコードでは、NavigationManager 抽象化だけを使っていることに注意してください。

PropertyInjection.razor:

@page "/property-injection"
@inject NavigationManager Navigation

<button @onclick="@(() => Navigation.NavigateTo("/counter"))">
    Take me to the Counter component
</button>

内部的には、生成されたプロパティ (Navigation) によって、[Inject] 属性が使用されます。 通常、この属性を直接使用することはありません。 コンポーネントで基底クラスが必要であり、基底クラスで挿入されたプロパティも必要な場合は、[Inject] 属性を手動で追加します。

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    ...
}

メモ

挿入されたサービスは使用できるようになると予想されるため、null を許容する演算子 (default!) を持つ既定のリテラルが .NET 6 以降で割り当てられます。 詳細については、「null 許容参照型 (NRT) と .NET コンパイラの null 状態の静的分析」を参照してください。

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。 基底クラスの InjectAttribute で十分です。 このコンポーネントには、@inherits ディレクティブのみが必要です。 次の例で、CustomComponentBase の挿入されたサービスはすべて、Demo コンポーネントに使用できます。

@page "/demo"
@inherits CustomComponentBase

サービスで DI を使用する

複雑なサービスでは、追加のサービスが必要になる場合があります。 次の例では、DataAccessHttpClient の既定のサービスが必要です。 @inject (または [Inject] 属性) は、サービスでは使用できません。 代わりに、"コンストラクター挿入" を使用する必要があります。 サービスのコンストラクターにパラメーターを追加することによって、必要なサービスが追加されます。 DI では、サービスを作成するときに、コンストラクターで必要なサービスが認識され、それに応じてサービスが提供されます。 次の例では、コンストラクターは DI で HttpClient を受け取ります。 HttpClient は既定のサービスです。

using System.Net.Http;

public class DataAccess : IDataAccess
{
    public DataAccess(HttpClient http)
    {
        ...
    }

    ...
}

コンストラクターの挿入は、C# 12 (.NET 8) 以降でのプライマリ コンストラクターでサポートされています。

using System.Net.Http;

public class DataAccess(HttpClient http) : IDataAccess
{
    ...
}

コンストラクター挿入の前提条件:

  • DI によってすべての引数を満たすことができるコンストラクターが 1 つ存在する必要があります。 DI で満たすことができない追加のパラメーターは、既定値が指定されている場合に許可されます。
  • 該当するコンストラクターは、public である必要があります。
  • 該当するコンストラクターが 1 つ存在する必要があります。 あいまいさがある場合は、DI で例外がスローされます。

キー付きサービスをコンポーネントに挿入する

Blazor では、[Inject] 属性を使ったキー付きサービスの挿入がサポートされています。 キーを利用すると、依存関係の挿入を使うときに、サービスの登録と使用のスコープを設定できます。 InjectAttribute.Key プロパティを使用して、挿入するサービスのキーを指定します:

[Inject(Key = "my-service")]
public IMyService MyService { get; set; }

DI スコープを管理するためのユーティリティの基本コンポーネント クラス

Blazor ASP.NET Core アプリ以外では、スコープ付きのサービスと一時的なサービスは通常、現在の要求にスコープ指定されます。 要求が完了すると、スコープ付きのサービスと一時的なサービスは DI システムによって破棄されます。

対話型のサーバー側 Blazor アプリでは、回線 (クライアントとサーバー間の SignalR 接続) がある間は DI スコープが存続します。そのため、スコープが指定された破棄可能な一時サービスが、1 つのコンポーネントの有効期間よりもはるかに長く存続する可能性があります。 そのため、サービスの有効期間をコンポーネントの有効期間と一致させる場合は、スコープ付きのサービスをコンポーネントに直接挿入しないでください。 IDisposable を実装していないコンポーネントに挿入された一時サービスは、コンポーネントが破棄されるときにガベージ コレクションされます。 ただし、"IDisposable を実装する" 挿入された一時サービスは、回路の有効期間中は DI コンテナーによって維持されるため、コンポーネントが破棄されるときにサービスのガベージ コレクションが行われず、メモリ リークが発生します。 OwningComponentBase 型に基づいたスコープ付きのサービスの代替アプローチについては、このセクションの後半で説明します。破棄可能な一時サービスはまったく使わないでください。 詳細については、「Blazor Server 上の一時的な破棄を解決するための設計 (dotnet/aspnetcore #26676)」を参照してください。

回線上で動作しないクライアント側 Blazor アプリでも、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ付きサービスより長く存続します。 クライアント側の破棄可能な一時サービスは、それらが挿入されたコンポーネントよりも長く存続します。その理由は、破棄可能なサービスへの参照を保持する DI コンテナーがアプリの有効期間中は存続し、サービスのガベージ コレクションが行われないためです。 存続期間が長い破棄可能な一時サービスはサーバー上でより大きな懸念事項になりますが、クライアント サービスの登録としても避ける必要があります。 サービスの有効期間を制御するために、クライアント側のスコープ付きのサービスでも OwningComponentBase 型を使うことが推奨されます。また、破棄可能な一時サービスはまったく使わないでください。

サービスの有効期間を制限する方法には OwningComponentBase 型を使用します。 OwningComponentBaseComponentBase から派生された抽象型であり、"コンポーネントの有効期間" に対応する DI スコープを作成します。 このスコープを使うと、コンポーネントはスコープ付きの有効期間でサービスを挿入し、コンポーネントと同じ期間だけ存続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、コンポーネント内では再利用され、コンポーネント間では共有されないサービスの場合に役立ちます。

2 つのバージョンの OwningComponentBase 型を使用でき、次の 2 つのセクションで説明されています。

OwningComponentBase

OwningComponentBase は、ComponentBase 型の抽象的で破棄可能な子であり、IServiceProvider型の保護された ScopedServices プロパティがあります。 このプロバイダーを使用すると、コンポーネントの有効期間にスコープが設定されているサービスを解決できます。

@inject または [Inject] 属性 を使用してコンポーネントに挿入された DI サービスは、コンポーネントのスコープでは作成されません。 コンポーネントのスコープを使用するには、GetRequiredService または GetServiceScopedServices を使用してサービスを解決する必要があります。 ScopedServices プロバイダーを使用して解決されたすべてのサービスには、コンポーネントのスコープで提供される依存関係があります。

次の例は、スコープ付きサービスを直接挿入した場合と、サーバーの ScopedServices を使用してサービスを解決した場合の違いを示しています。 タイム トラベル クラスの次のインターフェイスと実装には、DateTime 値を保持する DT プロパティが含まれています。 TimeTravel クラスがインスタンス化されると、実装によって DateTime.Now が呼び出され、DT が設定されます。

ITimeTravel.cs:

public interface ITimeTravel
{
    public DateTime DT { get; set; }
}

TimeTravel.cs:

public class TimeTravel : ITimeTravel
{
    public DateTime DT { get; set; } = DateTime.Now;
}

サービスは、サーバー側 Program ファイルのスコープとして登録されます。 サーバー側のスコープ サービスの有効期間は、回線の期間と同じです。

Program ファイルでは:

builder.Services.AddScoped<ITimeTravel, TimeTravel>();

次の TimeTravel コンポーネントでは、以下のことを行います。

  • タイム トラベル サービスは、TimeTravel1 として @inject で直接挿入されます。
  • サービスは、TimeTravel2 として ScopedServicesGetRequiredService でも個別に解決されます。

TimeTravel.razor:

@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}
@page "/time-travel"
@inject ITimeTravel TimeTravel1
@inherits OwningComponentBase

<h1><code>OwningComponentBase</code> Example</h1>

<ul>
    <li>TimeTravel1.DT: @TimeTravel1?.DT</li>
    <li>TimeTravel2.DT: @TimeTravel2?.DT</li>
</ul>

@code {
    private ITimeTravel TimeTravel2 { get; set; } = default!;

    protected override void OnInitialized()
    {
        TimeTravel2 = ScopedServices.GetRequiredService<ITimeTravel>();
    }
}

最初に TimeTravel コンポーネントに移動すると、タイム トラベル サービスはコンポーネントの読み込み時に 2 回インスタンス化され、TimeTravel1TimeTravel2 の初期値が同じになります。

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:45 PM

TimeTravel コンポーネントから別のコンポーネントに移動し、TimeTravel コンポーネントに戻る場合:

  • TimeTravel1 には、コンポーネントが最初に読み込まれたときに作成されたのと同じサービス インスタンスが提供されるため、DT の値は変わりません。
  • TimeTravel2 は、新しい DT 値を使用して TimeTravel2 に新しい ITimeTravel サービス インスタンスを取得します。

TimeTravel1.DT: 8/31/2022 2:54:45 PM
TimeTravel2.DT: 8/31/2022 2:54:48 PM

TimeTravel1 はユーザーの回線に関連付けられています。これはそのまま残り、基になる回線が分解されるまで破棄されません。 たとえば、切断された回線の保持期間に回線が切断された場合、サービスは破棄されます。

Program ファイルのスコープ付きサービスの登録とユーザーの回線の有効期間にかかわらず、コンポーネントが初期化されるたびに TimeTravel2 は新しい ITimeTravel サービス インスタンスを受け取ります。

OwningComponentBase<TService>

OwningComponentBase から派生する OwningComponentBase<TService> では、スコープ DI プロバイダーから T のインスタンスを返すプロパティ Service が追加されます。 この型は、アプリで 1 つのプライマリ サービスをコンポーネントのスコープを使用して DI コンテナーに要求するときに、IServiceProvider のインスタンスを使用せずにスコープ サービスにアクセスするための便利な方法です。 ScopedServices プロパティを使用できるので、必要に応じて、アプリで他の型のサービスを取得できます。

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>

クライアント側の一時的な破棄を検出する

カスタム コードをクライアント側 Blazor アプリに追加して、アプリ内で OwningComponentBase を使用する必要がある、破棄可能で一時的なサービスを検出できます。 このアプローチは、将来的にアプリに追加されるコードが、ライブラリによって追加されたサービスを含む 1 つ以上の一時的で破棄可能なサービスを使用することが懸念される場合に役立ちます。 デモ コードはBlazor サンプル GitHub リポジトリ (ダウンロード方法) にあります。

.NET 6 以降のバージョンのBlazorSample_WebAssembly サンプルで、次の内容を調べます。

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • In: Program.cs
    • アプリの Services 名前空間が、ファイルの先頭に用意されています (using BlazorSample.Services;)。
    • DetectIncorrectUsageOfTransients は、builderWebAssemblyHostBuilder.CreateDefault から割り当てられた直後に呼び出されます。
    • TransientDisposableService が登録されます (builder.Services.AddTransient<TransientDisposableService>();)。
    • EnableTransientDisposableDetection が、アプリの処理パイプライン内のビルドされたホスト (host.EnableTransientDisposableDetection();) で呼び出されます。
  • アプリによって、例外をスローせずに TransientDisposableService サービスが登録されます。 ただし、TransientService.razor でサービスを解決しようとすると、フレームワークが TransientDisposableService のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

サーバー側の一時的な破棄可能なアイテムを検出する

カスタム コードをサーバー側 Blazor アプリに追加して、アプリ内で OwningComponentBase を使用する必要がある、サーバー側の破棄可能で一時的なサービスを検出できます。 このアプローチは、将来的にアプリに追加されるコードが、ライブラリによって追加されたサービスを含む 1 つ以上の一時的で破棄可能なサービスを使用することが懸念される場合に役立ちます。 デモ コードはBlazor サンプル GitHub リポジトリ (ダウンロード方法) にあります。

.NET 8 以降のバージョンの BlazorSample_BlazorWebApp サンプルで、次の内容を調べます。

.NET 6 または .NET 7 のバージョンの BlazorSample_Server サンプルで、次の内容を調べます。

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs:
  • In: Program.cs
    • アプリの Services 名前空間が、ファイルの先頭に用意されています (using BlazorSample.Services;)。
    • DetectIncorrectUsageOfTransients が、ホスト ビルダー (builder.DetectIncorrectUsageOfTransients();) で呼び出されます。
    • TransientDependency サービスが登録されます (builder.Services.AddTransient<TransientDependency>();)。
    • TransitiveTransientDisposableDependencyITransitiveTransientDisposableDependency (builder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();) に登録されます。
  • アプリによって、例外をスローせずに TransientDependency サービスが登録されます。 ただし、TransientService.razor でサービスを解決しようとすると、フレームワークが TransientDependency のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

IHttpClientFactory/HttpClient ハンドラーの一時的なサービス登録

IHttpClientFactory/HttpClient ハンドラーの一時サービス登録をお勧めします。 アプリが IHttpClientFactory/HttpClient ハンドラーを含んでおり、IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> を使用して認証のサポートを追加する場合は、クライアント側認証用の次の一時的な破棄も検出されますが、これは想定されたもので、無視して構いません。

IHttpClientFactory/HttpClient の他のインスタンスも検出されます。 これらのインスタンスは無視することもできます。

Blazor サンプル GitHub リポジトリ (ダウンロード方法) の Blazor サンプル アプリは、一時的な破棄可能を検出するコードを示しています。 ただし、サンプル アプリには IHttpClientFactory/HttpClient ハンドラーが含まれているため、コードは非アクティブ化されています。

デモ コードをアクティブにしてその操作を確認するには、以下の手順を実行します。

  • Program.cs 内の一時的な破棄可能の行のコメントを解除します。

  • アプリのナビゲーション サイドバーに TransientService コンポーネントが表示されないようにする、NavLink.razor の条件チェックを削除します。

    - else if (name != "TransientService")
    + else
    
  • サンプル アプリを実行し、/transient-service にある TransientService コンポーネントに移動します。

DI からの Entity Framework Core (EF Core) DbContext の使用

詳細については、「ASP.NET Core Blazor と Entity Framework Core (EF Core)」をご覧ください。

異なる DI スコープからサーバー側の Blazor サービスにアクセスする

回線アクティビティ ハンドラーには、Blazor ではない依存関係の挿入 (DI) スコープ (IHttpClientFactory を使って作成されたスコープなど) からスコープ付き Blazor サービスにアクセスするためのアプローチが用意されています。

.NET 8 の ASP.NET Core がリリースされる前は、他の依存関係の挿入スコープから回線スコープ付きサービスにアクセスするには、カスタム ベース コンポーネントの種類を使う必要がありました。 回線アクティビティ ハンドラーを使う場合、次の例で示すようにカスタム ベース コンポーネントの種類は必要ありません。

public class CircuitServicesAccessor
{
    static readonly AsyncLocal<IServiceProvider> blazorServices = new();

    public IServiceProvider? Services
    {
        get => blazorServices.Value;
        set => blazorServices.Value = value;
    }
}

public class ServicesAccessorCircuitHandler(
    IServiceProvider services, CircuitServicesAccessor servicesAccessor) 
    : CircuitHandler
{
    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next) => 
            async context =>
            {
                servicesAccessor.Services = services;
                await next(context);
                servicesAccessor.Services = null;
            };
}

public static class CircuitServicesServiceCollectionExtensions
{
    public static IServiceCollection AddCircuitServicesAccessor(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitServicesAccessor>();
        services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

        return services;
    }
}

必要な場所に CircuitServicesAccessor を挿入して、回線スコープ付きサービスにアクセスします。

IHttpClientFactory を使って DelegatingHandler の設定から AuthenticationStateProvider にアクセスする方法を示す例については、「サーバーサイド ASP.NET Core Blazor のセキュリティに関するその他のシナリオ」を参照してください。

Razor コンポーネントが、別の DI スコープでコードを実行する非同期メソッドを呼び出す場合があります。 正しいアプローチを使用しないと、これらの DI スコープで Blazor のサービスにアクセスできません (IJSRuntimeMicrosoft.AspNetCore.Components.Server.ProtectedBrowserStorage など)。

たとえば、IHttpClientFactory を使って作成された HttpClient インスタンスには、独自の DI サービス スコープがあります。 その結果、HttpClient で構成された HttpMessageHandler インスタンスは、Blazor サービスを直接挿入できません。

AsyncLocal を定義するクラス BlazorServiceAccessor を作成します。これは、現在の非同期コンテキストの BlazorIServiceProvider を格納します。 BlazorServiceAccessor インスタンスを別の DI サービス スコープ内から取得して、Blazor サービスにアクセスできます。

BlazorServiceAccessor.cs:

internal sealed class BlazorServiceAccessor
{
    private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();

    public IServiceProvider? Services
    {
        get => s_currentServiceHolder.Value?.Services;
        set
        {
            if (s_currentServiceHolder.Value is { } holder)
            {
                // Clear the current IServiceProvider trapped in the AsyncLocal.
                holder.Services = null;
            }

            if (value is not null)
            {
                // Use object indirection to hold the IServiceProvider in an AsyncLocal
                // so it can be cleared in all ExecutionContexts when it's cleared.
                s_currentServiceHolder.Value = new() { Services = value };
            }
        }
    }

    private sealed class BlazorServiceHolder
    {
        public IServiceProvider? Services { get; set; }
    }
}

async コンポーネント メソッドが呼び出されたときに BlazorServiceAccessor.Services の値を自動的に設定するには、Razor コンポーネント コードに 3 つの主要な非同期エントリ ポイントを再実装するカスタム基本コンポーネントを作成します。

次のクラスは、基本コンポーネントの実装を示しています。

CustomComponentBase.cs:

using Microsoft.AspNetCore.Components;

public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
    private bool hasCalledOnAfterRender;

    [Inject]
    private IServiceProvider Services { get; set; } = default!;

    [Inject]
    private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;

    public override Task SetParametersAsync(ParameterView parameters)
        => InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));

    Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        => InvokeWithBlazorServiceContext(() =>
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        });

    Task IHandleAfterRender.OnAfterRenderAsync()
        => InvokeWithBlazorServiceContext(() =>
        {
            var firstRender = !hasCalledOnAfterRender;
            hasCalledOnAfterRender |= true;

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

    private async Task CallStateHasChangedOnAsyncCompletion(Task task)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        StateHasChanged();
    }

    private async Task InvokeWithBlazorServiceContext(Func<Task> func)
    {
        try
        {
            BlazorServiceAccessor.Services = Services;
            await func();
        }
        finally
        {
            BlazorServiceAccessor.Services = null;
        }
    }
}

CustomComponentBase を拡張するすべてのコンポーネントでは、BlazorServiceAccessor.Services が現在の Blazor DI スコープの IServiceProvider に自動的に設定されます。

最後に、Program ファイルで、スコープ付きサービスとして BlazorServiceAccessor を追加します:

builder.Services.AddScoped<BlazorServiceAccessor>();

最後に、Startup.csStartup.ConfigureServicesBlazorServiceAccessor をスコープ付きサービスとして追加します。

services.AddScoped<BlazorServiceAccessor>();

その他のリソース