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

作成者: Rainer StropekMike RousosBy Rainer Stropek and Mike Rousos

依存関係の挿入 (DI) は、中央の場所で構成されたサービスにアクセスするための手法です。Dependency injection (DI) is a technique for accessing services configured in a central location:

  • フレームワークによって登録されたサービスは、Blazor アプリのコンポーネントに直接挿入できます。Framework-registered services can be injected directly into components of Blazor apps.
  • Blazor アプリによって、カスタム サービスの定義と登録が行われ、DI を通じてアプリ全体でそれらが使用できるようになります。Blazor apps define and register custom services and make them available throughout the app via DI.

既定のサービスDefault services

Blazor アプリでよく使用されるサービスを次の表に示します。The services shown in the following table are commonly used in Blazor apps.

サービスService 有効期間Lifetime 説明Description
HttpClient スコープScoped

URI によって識別されるリソースに HTTP 要求を送信し、そのリソースから HTTP 応答を受信するためのメソッドが提供されます。Provides methods for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.

Blazor WebAssembly アプリの HttpClient のインスタンスでは、バックグラウンドでの HTTP トラフィックの処理にブラウザーが使用されます。The instance of HttpClient in a Blazor WebAssembly app uses the browser for handling the HTTP traffic in the background.

Blazor Server アプリには、既定でサービスとして構成される HttpClient は含まれません。Blazor Server apps don't include an HttpClient configured as a service by default. Blazor Server アプリには HttpClient を指定します。Provide an HttpClient to a Blazor Server app.

詳細については、「ASP.NET Core Blazor WebAssembly から Web API を呼び出す」を参照してください。For more information, see ASP.NET Core Blazor WebAssembly から Web API を呼び出す.

HttpClient は、シングルトンではなく、スコープ サービスとして登録されます。An HttpClient is registered as a scoped service, not singleton. 詳細については、「サービスの有効期間」セクションを参照してください。For more information, see the Service lifetime section.

IJSRuntime

Blazor WebAssembly :シングルトンBlazor WebAssembly: Singleton

Blazor Server :スコープBlazor Server: Scoped

JavaScript の呼び出しがディスパッチされる JavaScript ランタイムのインスタンスを表します。Represents an instance of a JavaScript runtime where JavaScript calls are dispatched. 詳細については、「ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す」を参照してください。For more information, see ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す.
NavigationManager

Blazor WebAssembly :シングルトンBlazor WebAssembly: Singleton

Blazor Server :スコープBlazor Server: Scoped

URI とナビゲーション状態を操作するためのヘルパーが含まれます。Contains helpers for working with URIs and navigation state. 詳細については、「URI およびナビゲーション状態ヘルパー」を参照してください。For more information, see URI and navigation state helpers.

カスタム サービス プロバイダーでは、表に示されている既定のサービスは自動的に提供されません。A custom service provider doesn't automatically provide the default services listed in the table. カスタム サービス プロバイダーを使用し、表に示されているいずれかのサービスが必要な場合は、必要なサービスを新しいサービス プロバイダーに追加します。If you use a custom service provider and require any of the services shown in the table, add the required services to the new service provider.

サービスをアプリに追加するAdd services to an app

Program.csProgram.Main メソッドで、アプリのサービス コレクション用のサービスを構成します。Configure services for the app's service collection in the Program.Main method of Program.cs. 次の例では、MyDependency の実装が IMyDependency に登録されます。In the following example, the MyDependency implementation is registered for IMyDependency:

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

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

ホストが構築されると、コンポーネントがレンダリングされる前に、ルート DI スコープからサービスを使用できるようになります。After the host is built, services are available from the root DI scope before any components are rendered. これは、コンテンツをレンダリングする前に初期化ロジックを実行する場合に役に立ちます。This can be useful for running initialization logic before rendering content:

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

ホストによって、アプリの中央構成インスタンスが提供されます。The host provides a central configuration instance for the app. 前の例を基にして、天気予報サービスの URL を、既定の構成ソース (appsettings.json など) から InitializeWeatherAsync に渡します。Building on the preceding example, the weather service's URL is passed from a default configuration source (for example, appsettings.json) to 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();
    }
}

新しいアプリを作成した後、Startup.csStartup.ConfigureServices メソッドを調べます。After creating a new app, examine the Startup.ConfigureServices method in Startup.cs:

using Microsoft.Extensions.DependencyInjection;

...

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

ConfigureServices メソッドには、サービス記述子オブジェクトのリストである IServiceCollection が渡されます。The ConfigureServices method is passed an IServiceCollection, which is a list of service descriptor objects. サービスは、ConfigureServices メソッドでサービス コレクションにサービス記述子を提供することによって追加されます。Services are added in the ConfigureServices method by providing service descriptors to the service collection. 次の例では、IDataAccess インターフェイスとその具象実装 DataAccess での概念を示します。The following example demonstrates the concept with the IDataAccess interface and its concrete implementation DataAccess:

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

サービスの有効期間Service lifetime

サービスは、次の表に示す有効期間で構成できます。Services can be configured with the lifetimes shown in the following table.

有効期間Lifetime 説明Description
Scoped

現在、Blazor WebAssembly アプリには DI スコープの概念はありません。Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped 登録済みサービスは Singleton サービスのように動作します。Scoped-registered services behave like Singleton services.

Blazor Server ホスティング モデルでは、HTTP 要求間で Scoped 有効期間がサポートされていますが、クライアントに読み込まれるコンポーネント間での SignalR 接続/回線メッセージ間ではサポートされていません。The Blazor Server hosting model supports the Scoped lifetime across HTTP requests but not across SignalR connection/circuit messages among components that are loaded on the client. アプリの Razor ページまたは MVC の部分では、スコープ付きサービスが通常どおりに処理され、ページまたはビュー間を移動するとき、またはページやビューからコンポーネントに移動するときに、"各 HTTP 要求" に対してサービスが再作成されます。The Razor Pages or MVC portion of the app treats scoped services normally and recreates the services on each HTTP request when navigating among pages or views or from a page or view to a component. クライアント上のコンポーネント間を移動するときは、スコープ付きサービスは再構築されません。この場合、サーバーとの通信は、HTTP 要求ではなく、ユーザーの回線の SignalR 接続を介して行われます。Scoped services aren't reconstructed when navigating among components on the client, where the communication to the server takes place over the SignalR connection of the user's circuit, not via HTTP requests. 次のクライアント上のコンポーネント シナリオでは、ユーザー用に新しい回線が作成されるため、スコープ付きサービスは再構築されます。In the following component scenarios on the client, scoped services are reconstructed because a new circuit is created for the user:

  • ユーザーがブラウザーのウィンドウを閉じる場合。The user closes the browser's window. ユーザーは新しいウィンドウを開き、アプリに戻ります。The user opens a new window and navigates back to the app.
  • ユーザーが、ブラウザー ウィンドウでアプリの最後のタブを閉じる場合。The user closes the last tab of the app in a browser window. ユーザーは新しいタブを開き、アプリに戻ります。The user opens a new tab and navigates back to the app.
  • ユーザーが、ブラウザーの再読み込みまたは更新ボタンを選択する場合。The user selects the browser's reload/refresh button.

Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法の詳細については、「ASP.NET Core Blazor のホスティング モデル」を参照してください。For more information on preserving user state across scoped services in Blazor Server apps, see ASP.NET Core Blazor のホスティング モデル.

Singleton DI では、サービスの "単一インスタンス" が作成されます。DI creates a single instance of the service. Singleton サービスを必要とするすべてのコンポーネントは、同じサービスのインスタンスを受け取ります。All components requiring a Singleton service receive an instance of the same service.
Transient コンポーネントは、サービス コンテナーから Transient サービスのインスタンスを取得するたびに、サービスの "新しいインスタンス" を受け取ります。Whenever a component obtains an instance of a Transient service from the service container, it receives a new instance of the service.

DI システムは、ASP.NET Core の DI システムが基になっています。The DI system is based on the DI system in ASP.NET Core. 詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。For more information, see ASP.NET Core での依存関係の挿入.

コンポーネント内のサービスを要求するRequest a service in a component

サービスがサービス コレクションに追加された後、@inject Razor ディレクティブを使用して、サービスをコンポーネントに挿入します。これには 2 つのパラメーターがあります。After services are added to the service collection, inject the services into the components using the @inject Razor directive, which has two parameters:

  • 型:挿入するサービスの型。Type: The type of the service to inject.
  • プロパティ:挿入されたアプリ サービスを受け取るプロパティの名前。Property: The name of the property receiving the injected app service. プロパティを手動で作成する必要はありません。The property doesn't require manual creation. プロパティはコンパイラによって作成されます。The compiler creates the property.

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

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

次の例は、@inject を使用する方法を示しています。The following example shows how to use @inject. Services.IDataAccess を実装するサービスを、コンポーネントのプロパティ DataRepository に挿入します。The service implementing Services.IDataAccess is injected into the component's property DataRepository. コードによって IDataAccess 抽象化だけが使用されていることに注意してください。Note how the code is only using the IDataAccess abstraction:

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

内部的には、生成されたプロパティ (DataRepository) によって、[Inject] 属性が使用されます。Internally, the generated property (DataRepository) uses the [Inject] attribute. 通常、この属性を直接使用することはありません。Typically, this attribute isn't used directly. コンポーネントで基底クラスが必要であり、基底クラスで挿入されたプロパティも必要な場合は、[Inject] 属性を手動で追加します。If a base class is required for components and injected properties are also required for the base class, manually add the [Inject] attribute:

using Microsoft.AspNetCore.Components;

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

    ...
}

基底クラスから派生されたコンポーネントでは、@inject ディレクティブは必要ありません。In components derived from the base class, the @inject directive isn't required. 基底クラスの InjectAttribute で十分です。The InjectAttribute of the base class is sufficient:

@page "/demo"
@inherits ComponentBase

<h1>Demo Component</h1>

サービスで DI を使用するUse DI in services

複雑なサービスでは、追加のサービスが必要になる場合があります。Complex services might require additional services. 次の例では、DataAccessHttpClient の既定のサービスが必要です。In the following example, DataAccess requires the HttpClient default service. @inject (または [Inject] 属性) は、サービスでは使用できません。@inject (or the [Inject] attribute) isn't available for use in services. 代わりに、"コンストラクター挿入" を使用する必要があります。Constructor injection must be used instead. サービスのコンストラクターにパラメーターを追加することによって、必要なサービスが追加されます。Required services are added by adding parameters to the service's constructor. DI では、サービスを作成するときに、コンストラクターで必要なサービスが認識され、それに応じてサービスが提供されます。When DI creates the service, it recognizes the services it requires in the constructor and provides them accordingly. 次の例では、コンストラクターは DI で HttpClient を受け取ります。In the following example, the constructor receives an HttpClient via DI. HttpClient は既定のサービスです。HttpClient is a default service.

using System.Net.Http;

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

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

  • DI によってすべての引数を満たすことができるコンストラクターが 1 つ存在する必要があります。One constructor must exist whose arguments can all be fulfilled by DI. DI で満たすことができない追加のパラメーターは、既定値が指定されている場合に許可されます。Additional parameters not covered by DI are allowed if they specify default values.
  • 該当するコンストラクターは、public である必要があります。The applicable constructor must be public.
  • 該当するコンストラクターが 1 つ存在する必要があります。One applicable constructor must exist. あいまいさがある場合は、DI で例外がスローされます。In case of an ambiguity, DI throws an exception.

DI スコープを管理するためのユーティリティの基本コンポーネント クラスUtility base component classes to manage a DI scope

ASP.NET Core アプリでは、スコープ サービスは通常、現在の要求にスコープされます。In ASP.NET Core apps, scoped services are typically scoped to the current request. 要求が完了すると、スコープ サービスまたは一時サービスは DI システムによって破棄されます。After the request completes, any scoped or transient services are disposed by the DI system. Blazor Server アプリでは、要求スコープはクライアント接続の期間を通して保持されるため、一時サービスとスコープ サービスが予想よりはるかに長く存続する可能性があります。In Blazor Server apps, the request scope lasts for the duration of the client connection, which can result in transient and scoped services living much longer than expected. Blazor WebAssembly アプリでは、スコープ付きの有効期間で登録されたサービスはシングルトンとして扱われるため、通常の ASP.NET Core アプリのスコープ サービスより長く存続します。In Blazor WebAssembly apps, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps.

注意

アプリ内の破棄可能な一時サービスを検出するには、「破棄可能な一時サービスの検出」セクションを参照してください。To detect disposable transient services in an app, see the Detect transient disposables section.

Blazor アプリでサービスの有効期間を制限するには、OwningComponentBase 型を使用します。An approach that limits a service lifetime in Blazor apps is use of the OwningComponentBase type. OwningComponentBaseComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。OwningComponentBase is an abstract type derived from ComponentBase that creates a DI scope corresponding to the lifetime of the component. このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。Using this scope, it's possible to use DI services with a scoped lifetime and have them live as long as the component. コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。When the component is destroyed, services from the component's scoped service provider are disposed as well. これは、次のようなサービスに役立ちます。This can be useful for services that:

  • 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。Should be reused within a component, as the transient lifetime is inappropriate.
  • シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。Shouldn't be shared across components, as the singleton lifetime is inappropriate.

OwningComponentBase 型には、使用できるバージョンが 2 つあります。Two versions of the OwningComponentBase type are available:

  • OwningComponentBase は、ComponentBase 型の抽象的で破棄可能な子であり、IServiceProvider型の保護された ScopedServices プロパティがあります。OwningComponentBase is an abstract, disposable child of the ComponentBase type with a protected ScopedServices property of type IServiceProvider. このプロバイダーを使用すると、コンポーネントの有効期間にスコープが設定されているサービスを解決できます。This provider can be used to resolve services that are scoped to the lifetime of the component.

    @inject または [Inject] 属性を使用してコンポーネントに挿入された DI サービスは、コンポーネントのスコープでは作成されません。DI services injected into the component using @inject or the [Inject] attribute aren't created in the component's scope. コンポーネントのスコープを使用するには、GetRequiredService または GetService を使用してサービスを解決する必要があります。To use the component's scope, services must be resolved using GetRequiredService or GetService. ScopedServices プロバイダーを使用して解決されたすべてのサービスには、同じスコープから提供される依存関係があります。Any services resolved using the ScopedServices provider have their dependencies provided from that same scope.

    @page "/preferences"
    @using Microsoft.Extensions.DependencyInjection
    @inherits OwningComponentBase
    
    <h1>User (@UserService.Name)</h1>
    
    <ul>
        @foreach (var setting in SettingService.GetSettings())
        {
            <li>@setting.SettingName: @setting.SettingValue</li>
        }
    </ul>
    
    @code {
        private IUserService UserService { get; set; }
        private ISettingService SettingService { get; set; }
    
        protected override void OnInitialized()
        {
            UserService = ScopedServices.GetRequiredService<IUserService>();
            SettingService = ScopedServices.GetRequiredService<ISettingService>();
        }
    }
    
    @page "/preferences"
    @using Microsoft.Extensions.DependencyInjection
    @inherits OwningComponentBase
    
    <h1>User (@UserService.Name)</h1>
    
    <ul>
        @foreach (var setting in SettingService.GetSettings())
        {
            <li>@setting.SettingName: @setting.SettingValue</li>
        }
    </ul>
    
    @code {
        private IUserService UserService { get; set; }
        private ISettingService SettingService { get; set; }
    
        protected override void OnInitialized()
        {
            UserService = ScopedServices.GetRequiredService<IUserService>();
            SettingService = ScopedServices.GetRequiredService<ISettingService>();
        }
    }
    
  • OwningComponentBase から派生する OwningComponentBase<TService> では、スコープ DI プロバイダーから T のインスタンスを返すプロパティ Service が追加されます。OwningComponentBase<TService> derives from OwningComponentBase and adds a Service property that returns an instance of T from the scoped DI provider. この型は、アプリで 1 つのプライマリ サービスをコンポーネントのスコープを使用して DI コンテナーに要求するときに、IServiceProvider のインスタンスを使用せずにスコープ サービスにアクセスするための便利な方法です。This type is a convenient way to access scoped services without using an instance of IServiceProvider when there's one primary service the app requires from the DI container using the component's scope. ScopedServices プロパティを使用できるので、必要に応じて、アプリで他の型のサービスを取得できます。The ScopedServices property is available, so the app can get services of other types, if necessary.

    @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 の使用Use of an Entity Framework Core (EF Core) DbContext from DI

詳細については、「ASP.NET Core Blazor Server と Entity Framework Core (EFCore)」を参照してください。For more information, see ASP.NET Core Blazor Server と Entity Framework Core (EFCore).

破棄可能な一時サービスの検出Detect transient disposables

次の例は、OwningComponentBase を使用する必要があるアプリ内の破棄可能な一時サービスを検出する方法を示しています。The following examples show how to detect disposable transient services in an app that should use OwningComponentBase. 詳細については、「DI スコープを管理するためのユーティリティの基本コンポーネント クラス」セクションを参照してください。For more information, see the Utility base component classes to manage a DI scope section.

DetectIncorrectUsagesOfTransientDisposables.cs: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; }
    }
}
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)。The TransientDisposable in the following example is detected (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();
}
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();
}

DetectIncorrectUsagesOfTransientDisposables.cs: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; }
    }
}
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 の名前空間を追加します。Add the namespace for Microsoft.Extensions.DependencyInjection to Program.cs:

using Microsoft.Extensions.DependencyInjection;

Program.csProgram.CreateHostBuilder で:In Program.CreateHostBuilder of Program.cs:

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

次の例では、TransientDependency が検出されます (Startup.cs)。The TransientDependency in the following example is detected (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;
    }
}

アプリは、例外をスローすることなく、一時的な破棄可能を登録できます。The app can register transient disposables without throwing an exception. ただし、次の例に示すように、一時的に破棄可能な結果を解決しようとすると、InvalidOperationException が発生します。However, attempting to resolve a transient disposable results in an InvalidOperationException, as the following example shows.

Pages/TransientDisposable.razor:Pages/TransientDisposable.razor:

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

<h1>Transient Disposable Detection</h1>

/transient-disposableTransientDisposable コンポーネントに移動すると、フレームワークが TransientDisposable のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。Navigate to the TransientDisposable component at /transient-disposable and an InvalidOperationException is thrown when the framework attempts to construct an instance of TransientDisposable:

System.InvalidOperationException:一時的に破棄可能なサービス TransientDisposable を間違ったスコープで解決しようとしています。System.InvalidOperationException: Trying to resolve transient disposable service TransientDisposable in the wrong scope. 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。Use an 'OwningComponentBase<T>' component base class for the service 'T' you are trying to resolve.

その他の技術情報Additional resources