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

作成者: Rainer StropekMike Rousos

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

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

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

Note

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

既定のサービス

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

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

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

Blazor WebAssembly アプリの HttpClient のインスタンスは、Program.cs でこのアプリによって登録されます。また、このインスタンスでは、バックグラウンドでの HTTP トラフィックの処理にブラウザーを使用します。

Blazor Server アプリには、既定でサービスとして構成される HttpClient は含まれません。 Blazor Server アプリには HttpClient を指定します。

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

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

IJSRuntime

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

サービスを Blazor WebAssembly アプリに追加する

Program.cs で、アプリのサービス コレクション用のサービスを構成します。 次の例では、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();

サービスを Blazor Server アプリに追加する

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

var builder = WebApplication.CreateBuilder(args);

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

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

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

ホストされている Blazor WebAssembly ソリューションに一般的なサービスを登録する

ホストされた Blazor WebAssembly ソリューションServer プロジェクトと Client プロジェクトで 1 つまたは複数の一般的なサービスが必要な場合は、Client プロジェクトのメソッドに一般的なサービス登録を配置し、メソッドを呼び出して両方のプロジェクトにサービスを登録できます。

まず、一般的なサービス登録を別のメソッドに組み込みます。 たとえば、Client プロジェクトで ConfigureCommonServices メソッドを作成します。

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

Client プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して共通サービスを登録します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Server プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して Server プロジェクトの共通サービスを登録します。

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

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

サービスの有効期間

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

有効期間 説明
Scoped

現在、Blazor WebAssembly アプリには DI スコープの概念はありません。 Scoped 登録済みサービスは Singleton サービスのように動作します。

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

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

Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法について詳しくは、「ASP.NET Core Blazor のホスティング モデル」をご覧ください。

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

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

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

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

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

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

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

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

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer>? customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

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

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected IDataAccess DataRepository { get; set; }

    ...
}

Note

挿入されたサービスは使用可能であると予想されるため、挿入されたサービスを Null 許容としてマークしないでください。 代わりに、null 免除演算子 (default!) を使って既定のリテラルを割り当てます。 次に例を示します。

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

詳細については、次のリソースを参照してください。

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。 基底クラスの InjectAttribute で十分です。

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

サービスで DI を使用する

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

using System.Net.Http;

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

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

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

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

ASP.NET Core アプリでは、スコープ サービスは通常、現在の要求にスコープされます。 要求が完了すると、スコープ サービスまたは一時サービスは DI システムによって破棄されます。 Blazor Server アプリでは、要求スコープはクライアント接続の期間を通して保持されるため、一時サービスとスコープ サービスが予想よりはるかに長く存続する可能性があります。 Blazor WebAssembly アプリでは、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ サービスより長く存続します。

Note

アプリ内の破棄可能な一時サービスを見つけるには、次のセクションを参照してください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出するBlazor Server アプリで破棄可能な一時サービスを検出する

Blazor アプリでサービスの有効期間を制限するには、OwningComponentBase 型を使用します。 OwningComponentBaseComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。 このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、次のようなサービスに役立ちます。

  • 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。
  • シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。

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

OwningComponentBase

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

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

次の例は、スコープ付きサービスを直接挿入した場合と、Blazor Server アプリで 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;
}

サービスは、Blazor Server アプリの Program.cs のスコープとして登録されます。 Blazor Server アプリでは、スコープ付きサービスの有効期間は、回線と呼ばれるクライアント接続の期間と同じです。

Program.csの場合:

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

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

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

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

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

この例をテスト アプリに配置する場合は、TimeTravel コンポーネントを NavMenu コンポーネントに追加します。

Shared/NavMenu.razorの場合:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="time-travel">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Time travel
    </NavLink>
</div>

最初に 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.cs のスコープ付きサービスの登録 とユーザーの回線の有効期間にかかわらず、コンポーネントが初期化されるたびに 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>

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

詳しくは、「EF Core」をご覧ください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

Blazor WebAssembly アプリの DetectIncorrectUsagesOfTransientDisposables.cs:

using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    
                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType?.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransientDisposable.cs:

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

次の例では、TransientDisposable が検出されます。

Program.cs:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp => 
    new HttpClient
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDisposable のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException:一時的に破棄可能なサービス TransientDisposable を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

注意

IHttpClientFactory ハンドラーの一時サービス登録をお勧めします。 このセクションの TransientExample コンポーネントは、認証を使用する Blazor WebAssembly アプリの次の一時的な破棄を示します。次のことが想定されています。

Blazor Server アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebApplicationBuilder DetectIncorrectUsageOfTransients(
            this WebApplicationBuilder builder)
        {
            builder.Host
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;

                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType?.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransitiveTransientDisposableDependency.cs:

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        this.transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

次の例では、TransientDependency が検出されます。

Program.csの場合:

builder.DetectIncorrectUsageOfTransients();
builder.Services.AddTransient<TransientDependency>();
builder.Services.AddTransient<ITransitiveTransientDisposableDependency, 
    TransitiveTransientDisposableDependency>();

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDependency のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException: 一時的に破棄可能なサービス TransientDependency を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

その他のリソース

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

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

Note

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

既定のサービス

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

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

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

Blazor WebAssembly アプリの HttpClient のインスタンスでは、バックグラウンドでの HTTP トラフィックの処理にブラウザーが使用されます。

Blazor Server アプリには、既定でサービスとして構成される HttpClient は含まれません。 Blazor Server アプリには HttpClient を指定します。

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

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

IJSRuntime

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

サービスを Blazor WebAssembly アプリに追加する

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

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        ...
        builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
        ...

        await builder.Build().RunAsync();
    }
}

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

public class Program
{
    public static async Task Main(string[] args)
    {
        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 に渡します。

public class Program
{
    public static async Task Main(string[] args)
    {
        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();
    }
}

サービスを Blazor Server アプリに追加する

新しいアプリを作成した後、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>();
}

ホストされている Blazor WebAssembly ソリューションに一般的なサービスを登録する

ホストされた Blazor WebAssembly ソリューションServer プロジェクトと Client プロジェクトで 1 つまたは複数の一般的なサービスが必要な場合は、Client プロジェクトのメソッドに一般的なサービス登録を配置し、メソッドを呼び出して両方のプロジェクトにサービスを登録できます。

まず、一般的なサービス登録を別のメソッドに組み込みます。 たとえば、Client プロジェクトで ConfigureCommonServices メソッドを作成します。

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

クライアント (Client) プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して共通サービスを登録します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Server プロジェクトの ConfigureServices メソッド (Startup.cs) で、ConfigureCommonServices を呼び出して Server プロジェクトの共通サービスを登録します。

Client.Program.ConfigureCommonServices(services);

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

サービスの有効期間

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

有効期間 説明
Scoped

現在、Blazor WebAssembly アプリには DI スコープの概念はありません。 Scoped 登録済みサービスは Singleton サービスのように動作します。

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

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

Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法について詳しくは、「ASP.NET Core Blazor のホスティング モデル」をご覧ください。

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

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

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

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

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

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

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

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

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

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

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected IDataAccess DataRepository { get; set; }

    ...
}

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。 基底クラスの InjectAttribute で十分です。

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

サービスで DI を使用する

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

using System.Net.Http;

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

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

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

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

ASP.NET Core アプリでは、スコープ サービスは通常、現在の要求にスコープされます。 要求が完了すると、スコープ サービスまたは一時サービスは DI システムによって破棄されます。 Blazor Server アプリでは、要求スコープはクライアント接続の期間を通して保持されるため、一時サービスとスコープ サービスが予想よりはるかに長く存続する可能性があります。 Blazor WebAssembly アプリでは、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ サービスより長く存続します。

Note

アプリ内の破棄可能な一時サービスを見つけるには、次のセクションを参照してください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出するBlazor Server アプリで破棄可能な一時サービスを検出する

Blazor アプリでサービスの有効期間を制限するには、OwningComponentBase 型を使用します。 OwningComponentBaseComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。 このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、次のようなサービスに役立ちます。

  • 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。
  • シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。

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

OwningComponentBase

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

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

次の例は、スコープ付きサービスを直接挿入した場合と、Blazor Server アプリで 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;
}

サービスは、Blazor Server アプリの Program.cs のスコープとして登録されます。 Blazor Server アプリでは、スコープ付きサービスの有効期間は、回線と呼ばれるクライアント接続の期間と同じです。

Program.csの場合:

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

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

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

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

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

この例をテスト アプリに配置する場合は、TimeTravel コンポーネントを NavMenu コンポーネントに追加します。

Shared/NavMenu.razorの場合:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="time-travel">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Time travel
    </NavLink>
</div>

最初に 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.cs のスコープ付きサービスの登録 とユーザーの回線の有効期間にかかわらず、コンポーネントが初期化されるたびに 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>

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

詳しくは、「EF Core」をご覧ください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

次の例では、TransientDisposable が検出されます (Program.cs)。

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.DetectIncorrectUsageOfTransients();
        builder.RootComponents.Add<App>("#app");

        builder.Services.AddTransient<TransientDisposable>();
        builder.Services.AddScoped(sp =>
            new HttpClient
            {
                BaseAddress = new(builder.HostEnvironment.BaseAddress)
            });

        var host = builder.Build();
        host.EnableTransientDisposableDetection();
        await host.RunAsync();
    }
}

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDisposable のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException:一時的に破棄可能なサービス TransientDisposable を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

注意

IHttpClientFactory ハンドラーの一時サービス登録をお勧めします。 このセクションの TransientExample コンポーネントは、認証を使用する Blazor WebAssembly アプリの次の一時的な破棄を示します。次のことが想定されています。

Blazor Server アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static IHostBuilder DetectIncorrectUsageOfTransients(
            this IHostBuilder builder)
        {
            builder
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Program.csMicrosoft.Extensions.DependencyInjection の名前空間を追加します。

using Microsoft.Extensions.DependencyInjection;

Program.csProgram.CreateHostBuilder で:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .DetectIncorrectUsageOfTransients()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

次の例では、TransientDependency が検出されます (Startup.cs)。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<TransientDependency>();
    services.AddTransient<ITransitiveTransientDisposableDependency, 
        TransitiveTransientDisposableDependency>();
}

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        _transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        _transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDependency のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException: 一時的に破棄可能なサービス TransientDependency を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

その他のリソース

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

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

Note

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

既定のサービス

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

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

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

Blazor WebAssembly アプリの HttpClient のインスタンスでは、バックグラウンドでの HTTP トラフィックの処理にブラウザーが使用されます。

Blazor Server アプリには、既定でサービスとして構成される HttpClient は含まれません。 Blazor Server アプリには HttpClient を指定します。

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

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

IJSRuntime

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

サービスを Blazor WebAssembly アプリに追加する

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

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        ...
        builder.Services.AddSingleton<IExampleDependency, ExampleDependency>();
        ...

        await builder.Build().RunAsync();
    }
}

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

public class Program
{
    public static async Task Main(string[] args)
    {
        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 に渡します。

public class Program
{
    public static async Task Main(string[] args)
    {
        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();
    }
}

サービスを Blazor Server アプリに追加する

新しいアプリを作成した後、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>();
}

ホストされている Blazor WebAssembly ソリューションに一般的なサービスを登録する

ホストされた Blazor WebAssembly ソリューションServer プロジェクトと Client プロジェクトで 1 つまたは複数の一般的なサービスが必要な場合は、Client プロジェクトのメソッドに一般的なサービス登録を配置し、メソッドを呼び出して両方のプロジェクトにサービスを登録できます。

まず、一般的なサービス登録を別のメソッドに組み込みます。 たとえば、Client プロジェクトで ConfigureCommonServices メソッドを作成します。

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

Client プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して共通サービスを登録します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Server プロジェクトの ConfigureServices メソッド (Startup.cs) で、ConfigureCommonServices を呼び出して Server プロジェクトの共通サービスを登録します。

Client.Program.ConfigureCommonServices(services);

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

サービスの有効期間

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

有効期間 説明
Scoped

現在、Blazor WebAssembly アプリには DI スコープの概念はありません。 Scoped 登録済みサービスは Singleton サービスのように動作します。

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

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

Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法について詳しくは、「ASP.NET Core Blazor のホスティング モデル」をご覧ください。

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

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

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

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

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

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

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

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

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer> customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

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

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected IDataAccess DataRepository { get; set; }

    ...
}

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。 基底クラスの InjectAttribute で十分です。

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

サービスで DI を使用する

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

using System.Net.Http;

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

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

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

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

ASP.NET Core アプリでは、スコープ サービスは通常、現在の要求にスコープされます。 要求が完了すると、スコープ サービスまたは一時サービスは DI システムによって破棄されます。 Blazor Server アプリでは、要求スコープはクライアント接続の期間を通して保持されるため、一時サービスとスコープ サービスが予想よりはるかに長く存続する可能性があります。 Blazor WebAssembly アプリでは、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ サービスより長く存続します。

Note

アプリ内の破棄可能な一時サービスを見つけるには、次のセクションを参照してください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出するBlazor Server アプリで破棄可能な一時サービスを検出する

Blazor アプリでサービスの有効期間を制限するには、OwningComponentBase 型を使用します。 OwningComponentBaseComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。 このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、次のようなサービスに役立ちます。

  • 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。
  • シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。

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

OwningComponentBase

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

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

次の例は、スコープ付きサービスを直接挿入した場合と、Blazor Server アプリで 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;
}

サービスは、Blazor Server アプリの Program.cs のスコープとして登録されます。 Blazor Server アプリでは、スコープ付きサービスの有効期間は、回線と呼ばれるクライアント接続の期間と同じです。

Program.csの場合:

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

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

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

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

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

この例をテスト アプリに配置する場合は、TimeTravel コンポーネントを NavMenu コンポーネントに追加します。

Shared/NavMenu.razorの場合:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="time-travel">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Time travel
    </NavLink>
</div>

最初に 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.cs のスコープ付きサービスの登録 とユーザーの回線の有効期間にかかわらず、コンポーネントが初期化されるたびに 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>

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

詳しくは、「EF Core」をご覧ください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

次の例では、TransientDisposable が検出されます (Program.cs)。

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.DetectIncorrectUsageOfTransients();
        builder.RootComponents.Add<App>("app");

        builder.Services.AddTransient<TransientDisposable>();
        builder.Services.AddScoped(sp =>
            new HttpClient
            {
                BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
            });

        var host = builder.Build();
        host.EnableTransientDisposableDetection();
        await host.RunAsync();
    }
}

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDisposable のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException:一時的に破棄可能なサービス TransientDisposable を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

注意

IHttpClientFactory ハンドラーの一時サービス登録をお勧めします。 このセクションの TransientExample コンポーネントは、認証を使用する Blazor WebAssembly アプリの次の一時的な破棄を示します。次のことが想定されています。

Blazor Server アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using System;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static IHostBuilder DetectIncorrectUsageOfTransients(
            this IHostBuilder builder)
        {
            builder
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

Program.csMicrosoft.Extensions.DependencyInjection の名前空間を追加します。

using Microsoft.Extensions.DependencyInjection;

Program.csProgram.CreateHostBuilder で:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .DetectIncorrectUsageOfTransients()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

次の例では、TransientDependency が検出されます (Startup.cs)。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<TransientDependency>();
    services.AddTransient<ITransitiveTransientDisposableDependency, 
        TransitiveTransientDisposableDependency>();
}

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        _transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        _transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDependency のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException: 一時的に破棄可能なサービス TransientDependency を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

その他のリソース

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

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

Note

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

既定のサービス

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

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

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

Blazor WebAssembly アプリの HttpClient のインスタンスは、Program.cs でこのアプリによって登録されます。また、このインスタンスでは、バックグラウンドでの HTTP トラフィックの処理にブラウザーを使用します。

Blazor Server アプリには、既定でサービスとして構成される HttpClient は含まれません。 Blazor Server アプリには HttpClient を指定します。

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

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

IJSRuntime

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

Blazor WebAssembly :シングルトン

Blazor Server :スコープ

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

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

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

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

サービスを Blazor WebAssembly アプリに追加する

Program.cs で、アプリのサービス コレクション用のサービスを構成します。 次の例では、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();

サービスを Blazor Server アプリに追加する

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

var builder = WebApplication.CreateBuilder(args);

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

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

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

ホストされている Blazor WebAssembly ソリューションに一般的なサービスを登録する

ホストされた Blazor WebAssembly ソリューションServer プロジェクトと Client プロジェクトで 1 つまたは複数の一般的なサービスが必要な場合は、Client プロジェクトのメソッドに一般的なサービス登録を配置し、メソッドを呼び出して両方のプロジェクトにサービスを登録できます。

まず、一般的なサービス登録を別のメソッドに組み込みます。 たとえば、Client プロジェクトで ConfigureCommonServices メソッドを作成します。

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

Client プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して共通サービスを登録します。

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

Server プロジェクトの Program.cs ファイルで、ConfigureCommonServices を呼び出して Server プロジェクトの共通サービスを登録します。

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

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

サービスの有効期間

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

有効期間 説明
Scoped

現在、Blazor WebAssembly アプリには DI スコープの概念はありません。 Scoped 登録済みサービスは Singleton サービスのように動作します。

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

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

Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法について詳しくは、「ASP.NET Core Blazor のホスティング モデル」をご覧ください。

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

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

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

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

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

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

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

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

@page "/customer-list"
@inject IDataAccess DataRepository

@if (customers != null)
{
    <ul>
        @foreach (var customer in customers)
        {
            <li>@customer.FirstName @customer.LastName</li>
        }
    </ul>
}

@code {
    private IReadOnlyList<Customer>? customers;

    protected override async Task OnInitializedAsync()
    {
        customers = await DataRepository.GetAllCustomersAsync();
    }

    private class Customer
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
    }

    private interface IDataAccess
    {
        public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
    }
}

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

using Microsoft.AspNetCore.Components;

public class ComponentBase : IComponent
{
    [Inject]
    protected IDataAccess DataRepository { get; set; }

    ...
}

Note

挿入されたサービスは使用可能であると予想されるため、挿入されたサービスを Null 許容としてマークしないでください。 代わりに、null 免除演算子 (default!) を使って既定のリテラルを割り当てます。 次に例を示します。

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

詳細については、次のリソースを参照してください。

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。 基底クラスの InjectAttribute で十分です。

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

サービスで DI を使用する

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

using System.Net.Http;

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

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

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

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

ASP.NET Core アプリでは、スコープ サービスは通常、現在の要求にスコープされます。 要求が完了すると、スコープ サービスまたは一時サービスは DI システムによって破棄されます。 Blazor Server アプリでは、要求スコープはクライアント接続の期間を通して保持されるため、一時サービスとスコープ サービスが予想よりはるかに長く存続する可能性があります。 Blazor WebAssembly アプリでは、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ サービスより長く存続します。

Note

アプリ内の破棄可能な一時サービスを見つけるには、次のセクションを参照してください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出するBlazor Server アプリで破棄可能な一時サービスを検出する

Blazor アプリでサービスの有効期間を制限するには、OwningComponentBase 型を使用します。 OwningComponentBaseComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。 このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、次のようなサービスに役立ちます。

  • 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。
  • シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。

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

OwningComponentBase

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

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

次の例は、スコープ付きサービスを直接挿入した場合と、Blazor Server アプリで 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;
}

サービスは、Blazor Server アプリの Program.cs のスコープとして登録されます。 Blazor Server アプリでは、スコープ付きサービスの有効期間は、回線と呼ばれるクライアント接続の期間と同じです。

Program.csの場合:

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

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

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

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

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

この例をテスト アプリに配置する場合は、TimeTravel コンポーネントを NavMenu コンポーネントに追加します。

Shared/NavMenu.razorの場合:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="time-travel">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Time travel
    </NavLink>
</div>

最初に 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.cs のスコープ付きサービスの登録 とユーザーの回線の有効期間にかかわらず、コンポーネントが初期化されるたびに 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>

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

詳しくは、「EF Core」をご覧ください。

Blazor WebAssembly アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

Blazor WebAssembly アプリの DetectIncorrectUsagesOfTransientDisposables.cs:

using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorWebAssemblyTransientDisposable;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
            this WebAssemblyHostBuilder builder)
        {
            builder
                .ConfigureContainer(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory());

            return builder;
        }

        public static WebAssemblyHost EnableTransientDisposableDetection(
            this WebAssemblyHost webAssemblyHost)
        {
            webAssemblyHost.Services
                .GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;

            return webAssemblyHost;
        }
    }
}

namespace BlazorWebAssemblyTransientDisposable
{
    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;
                    
                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                        "transient disposable service " +
                        $"{original.ImplementationType?.Name} in the wrong " +
                        "scope. Use an 'OwningComponentBase<T>' component base " +
                        "class for the service 'T' you are trying to resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransientDisposable.cs:

public class TransientDisposable : IDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

次の例では、TransientDisposable が検出されます。

Program.cs:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp => 
    new HttpClient
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDisposable TransientDisposable

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDisposable のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException:一時的に破棄可能なサービス TransientDisposable を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

注意

IHttpClientFactory ハンドラーの一時サービス登録をお勧めします。 このセクションの TransientExample コンポーネントは、認証を使用する Blazor WebAssembly アプリの次の一時的な破棄を示します。次のことが想定されています。

Blazor Server アプリで破棄可能な一時サービスを検出する

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。

DetectIncorrectUsagesOfTransientDisposables.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection
{
    using BlazorServerTransientDisposable;

    public static class WebHostBuilderTransientDisposableExtensions
    {
        public static WebApplicationBuilder DetectIncorrectUsageOfTransients(
            this WebApplicationBuilder builder)
        {
            builder.Host
                .UseServiceProviderFactory(
                    new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
                .ConfigureServices(
                    s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
                        ThrowOnTransientDisposableHandler>()));

            return builder;
        }
    }
}

namespace BlazorServerTransientDisposable
{
    internal class ThrowOnTransientDisposableHandler : CircuitHandler
    {
        public ThrowOnTransientDisposableHandler(
            ThrowOnTransientDisposable throwOnTransientDisposable)
        {
            throwOnTransientDisposable.ShouldThrow = true;
        }
    }

    public class DetectIncorrectUsageOfTransientDisposablesServiceFactory 
        : IServiceProviderFactory<IServiceCollection>
    {
        public IServiceCollection CreateBuilder(IServiceCollection services) => 
            services;

        public IServiceProvider CreateServiceProvider(
            IServiceCollection containerBuilder)
        {
            var collection = new ServiceCollection();

            foreach (var descriptor in containerBuilder)
            {
                if (descriptor.Lifetime == ServiceLifetime.Transient &&
                    descriptor.ImplementationType != null && 
                    typeof(IDisposable).IsAssignableFrom(
                        descriptor.ImplementationType))
                {
                    collection.Add(CreatePatchedDescriptor(descriptor));
                }
                else if (descriptor.Lifetime == ServiceLifetime.Transient &&
                         descriptor.ImplementationFactory != null)
                {
                    collection.Add(CreatePatchedFactoryDescriptor(descriptor));
                }
                else
                {
                    collection.Add(descriptor);
                }
            }

            collection.AddScoped<ThrowOnTransientDisposable>();

            return collection.BuildServiceProvider();
        }

        private ServiceDescriptor CreatePatchedFactoryDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) =>
                {
                    var originalFactory = original.ImplementationFactory;

                    if (originalFactory is null)
                    {
                        throw new InvalidOperationException(
                            "originalFactory is null.");
                    }

                    var originalResult = originalFactory(sp);

                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow && 
                        originalResult is IDisposable d)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            $"transient disposable service {d.GetType().Name} in " +
                            "the wrong scope. Use an 'OwningComponentBase<T>' " +
                            "component base class for the service 'T' you are " +
                            "trying to resolve.");
                    }

                    return originalResult;
                },
                original.Lifetime);

            return newDescriptor;
        }

        private ServiceDescriptor CreatePatchedDescriptor(
            ServiceDescriptor original)
        {
            var newDescriptor = new ServiceDescriptor(
                original.ServiceType,
                (sp) => {
                    var throwOnTransientDisposable = 
                        sp.GetRequiredService<ThrowOnTransientDisposable>();
                    if (throwOnTransientDisposable.ShouldThrow)
                    {
                        throw new InvalidOperationException("Trying to resolve " +
                            "transient disposable service " +
                            $"{original.ImplementationType?.Name} in the wrong " +
                            "scope. Use an 'OwningComponentBase<T>' component " +
                            "base class for the service 'T' you are trying to " +
                            "resolve.");
                    }

                    if (original.ImplementationType is null)
                    {
                        throw new InvalidOperationException(
                            "ImplementationType is null.");
                    }

                    return ActivatorUtilities.CreateInstance(sp, 
                        original.ImplementationType);
                },
                ServiceLifetime.Transient);
    
            return newDescriptor;
        }
    }

    internal class ThrowOnTransientDisposable
    {
        public bool ShouldThrow { get; set; }
    }
}

TransitiveTransientDisposableDependency.cs:

public class TransitiveTransientDisposableDependency 
    : ITransitiveTransientDisposableDependency, IDisposable
{
    public void Dispose() { }
}

public interface ITransitiveTransientDisposableDependency
{
}

public class TransientDependency
{
    private readonly ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency;

    public TransientDependency(ITransitiveTransientDisposableDependency 
        transitiveTransientDisposableDependency)
    {
        this.transitiveTransientDisposableDependency = 
            transitiveTransientDisposableDependency;
    }
}

次の例では、TransientDependency が検出されます。

Program.csの場合:

builder.DetectIncorrectUsageOfTransients();
builder.Services.AddTransient<TransientDependency>();
builder.Services.AddTransient<ITransitiveTransientDisposableDependency, 
    TransitiveTransientDisposableDependency>();

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。 ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。

Pages/TransientExample.razor:

@page "/transient-example"
@inject TransientDependency TransientDependency

<h1>Transient Disposable Detection</h1>

/transient-exampleTransientExample コンポーネントに移動すると、フレームワークが TransientDependency のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。

System.InvalidOperationException: 一時的に破棄可能なサービス TransientDependency を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。

その他のリソース