ASP.NET Core Blazor 依存関係の挿入
注意
これは、この記事の最新バージョンではありません。 最新のバージョンに切り替えるには、目次の上部にある ASP.NET Core バージョン セレクターを使用します。
セレクターが狭いブラウザー ウィンドウに表示されない場合は、ウィンドウの幅を広げるか、垂直省略記号 ([⋮]) >[目次] の順に選択します。
作成者: Rainer Stropek、Mike Rousos
この記事では、Blazor アプリでサービスをコンポーネントに挿入する方法について説明します。
依存関係の挿入 (DI) は、中央の場所で構成されたサービスにアクセスするための手法です。
- フレームワークによって登録されたサービスは、Blazor アプリのコンポーネントに直接挿入できます。
- Blazor アプリによって、カスタム サービスの定義と登録が行われ、DI を通じてアプリ全体でそれらが使用できるようになります。
Note
このトピックを読む前に、ASP.NET Core での依存関係の挿入に関する記事をお読みになることをお勧めします。
既定のサービス
Blazor アプリでよく使用されるサービスを次の表に示します。
サービス | 有効期間 | 説明 |
---|---|---|
HttpClient | スコープ | URI によって識別されるリソースに HTTP 要求を送信し、そのリソースから HTTP 応答を受信するためのメソッドが提供されます。 Blazor WebAssembly アプリの HttpClient のインスタンスは、 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 アプリのシングルトン サービスにサービスを挿入する場合は、次のいずれかの方法を使用します。
|
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>();
新しいアプリを作成した後、Startup.cs
で Startup.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 プロジェクトの Program.cs
ファイルで、ConfigureCommonServices
を呼び出して Server プロジェクトの共通サービスを登録します。
var builder = WebApplication.CreateBuilder(args);
...
Client.Program.ConfigureCommonServices(builder.Services);
この方法の例については、「ASP.NET Core Blazor WebAssembly のセキュリティに関するその他のシナリオ」を参照してください。
サービスの有効期間
サービスは、次の表に示す有効期間で構成できます。
有効期間 | 説明 |
---|---|
Scoped | 現在、Blazor WebAssembly アプリには DI スコープの概念はありません。 Blazor Server ホスティング モデルでは、HTTP 要求間で
Blazor Server アプリのスコープ付きサービス間でユーザー状態を保持する方法について詳しくは、「ASP.NET Core Blazor のホスティング モデル」をご覧ください。 |
Singleton | DI では、サービスの "単一インスタンス" が作成されます。 Singleton サービスを必要とするすべてのコンポーネントは、サービスの同じインスタンスを受け取ります。 |
Transient | コンポーネントは、サービス コンテナーから Transient サービスのインスタンスを取得するたびに、サービスの "新しいインスタンス" を受け取ります。 |
DI システムは、ASP.NET Core の DI システムが基になっています。 詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。
コンポーネント内のサービスを要求する
サービスがサービス コレクションに追加された後、@inject
Razor ディレクティブを使用して、サービスをコンポーネントに挿入します。これには 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 を使用する
複雑なサービスでは、追加のサービスが必要になる場合があります。 次の例では、DataAccess
に HttpClient の既定のサービスが必要です。 @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 型を使用します。 OwningComponentBase は ComponentBase から派生された抽象型であり、コンポーネントの有効期間に対応する DI スコープを作成します。 このスコープを使用すると、スコープ付きの有効期間で DI サービスを使用し、コンポーネントと同じ期間だけ持続させることができます。 コンポーネントが破棄されると、コンポーネントのスコープ サービス プロバイダーからのサービスも破棄されます。 これは、次のようなサービスに役立ちます。
- 一時的な有効期間が不適切であるため、コンポーネント内で再利用する必要がある。
- シングルトンの有効期間が不適切であるため、コンポーネント間で共有してはならない。
2 つのバージョンの OwningComponentBase 型を使用でき、次の 2 つのセクションで説明されています。
OwningComponentBase
OwningComponentBase は、ComponentBase 型の抽象的で破棄可能な子であり、IServiceProvider型の保護された ScopedServices プロパティがあります。 このプロバイダーを使用すると、コンポーネントの有効期間にスコープが設定されているサービスを解決できます。
@inject
または [Inject]
属性 を使用してコンポーネントに挿入された DI サービスは、コンポーネントのスコープでは作成されません。 コンポーネントのスコープを使用するには、GetRequiredService または GetService で ScopedServices を使用してサービスを解決する必要があります。 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
として ScopedServices と GetRequiredService でも個別に解決されます。
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>();
}
}
@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 回インスタンス化され、TimeTravel1
と TimeTravel2
の初期値が同じになります。
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 の使用
詳細については、「ASP.NET Core Blazor Server と Entity Framework Core (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; }
}
}
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; }
}
}
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.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();
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-example
で TransientExample
コンポーネントに移動すると、フレームワークが 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; }
}
}
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; }
}
}
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; }
}
}
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>();
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-example
で TransientExample
コンポーネントに移動すると、フレームワークが TransientDependency
のインスタンスを構築しようとしたときに InvalidOperationException がスローされます。
System.InvalidOperationException: 一時的に破棄可能なサービス TransientDependency を間違ったスコープで解決しようとしています。 解決しようとしているサービス 'T' に対して 'OwningComponentBase<T>' コンポーネントの基底クラスを使用してください。
別の DI スコープから Blazor サービスにアクセスする
"このセクションは Blazor Server アプリにのみ適用されます。 "
回線アクティビティ ハンドラーには、Blazor ではない依存関係の挿入 (DI) スコープ (IHttpClientFactory を使って作成されたスコープなど) からスコープ付き Blazor サービスにアクセスするためのアプローチが用意されています。
ASP.NET Core 8.0 がリリースされる前は、他の依存関係の挿入スコープから回線スコープ付きサービスにアクセスするには、カスタム ベース コンポーネントの種類を使う必要がありました。 回線アクティビティ ハンドラーを使う場合、次の例で示すようにカスタム ベース コンポーネントの種類は必要ありません。
public class CircuitServicesAccessor
{
static readonly AsyncLocal<IServiceProvider> blazorServices = new();
public IServiceProvider? Services
{
get => blazorServices.Value;
set => blazorServices.Value = value;
}
}
public class ServicesAccessorCircuitHandler : CircuitHandler
{
readonly IServiceProvider services;
readonly CircuitServicesAccessor circuitServicesAccessor;
public ServicesAccessorCircuitHandler(IServiceProvider services,
CircuitServicesAccessor servicesAccessor)
{
this.services = services;
this.circuitServicesAccessor = servicesAccessor;
}
public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next)
{
return async context =>
{
circuitServicesAccessor.Services = services;
await next(context);
circuitServicesAccessor.Services = null;
};
}
}
public static class CircuitServicesServiceCollectionExtensions
{
public static IServiceCollection AddCircuitServicesAccessor(
this IServiceCollection services)
{
services.AddScoped<CircuitServicesAccessor>();
services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();
return services;
}
}
必要な場所に CircuitServicesAccessor
を挿入して、回線スコープ付きサービスにアクセスします。
IHttpClientFactory を使って設定した DelegatingHandler から AuthenticationStateProvider にアクセスする方法を示す例については、「ASP.NET Core Blazor Server のセキュリティに関するその他のシナリオ」を参照してください。
Razor コンポーネントが、別の DI スコープでコードを実行する非同期メソッドを呼び出す場合があります。 正しいアプローチを使用しないと、これらの DI スコープで Blazor のサービスにアクセスできません (IJSRuntime や Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage など)。
たとえば、IHttpClientFactory を使って作成された HttpClient インスタンスには、独自の DI サービス スコープがあります。 その結果、HttpClient で構成された HttpMessageHandler インスタンスは、Blazor サービスを直接挿入できません。
AsyncLocal
を定義するクラス BlazorServiceAccessor
を作成します。これは、現在の非同期コンテキストの BlazorIServiceProvider を格納します。 BlazorServiceAcccessor
インスタンスを別の DI サービス スコープ内から取得して、Blazor サービスにアクセスできます。
BlazorServiceAccessor.cs
:
internal sealed class BlazorServiceAccessor
{
private static readonly AsyncLocal<BlazorServiceHolder> s_currentServiceHolder = new();
public IServiceProvider? Services
{
get => s_currentServiceHolder.Value?.Services;
set
{
if (s_currentServiceHolder.Value is { } holder)
{
// Clear the current IServiceProvider trapped in the AsyncLocal.
holder.Services = null;
}
if (value is not null)
{
// Use object indirection to hold the IServiceProvider in an AsyncLocal
// so it can be cleared in all ExecutionContexts when it's cleared.
s_currentServiceHolder.Value = new() { Services = value };
}
}
}
private sealed class BlazorServiceHolder
{
public IServiceProvider? Services { get; set; }
}
}
async
コンポーネント メソッドが呼び出されたときに BlazorServiceAccessor.Services
の値を自動的に設定するには、Razor コンポーネント コードに 3 つの主要な非同期エントリ ポイントを再実装するカスタム基本コンポーネントを作成します。
次のクラスは、基本コンポーネントの実装を示しています。
CustomComponentBase.cs
:
using Microsoft.AspNetCore.Components;
public class CustomComponentBase : ComponentBase, IHandleEvent, IHandleAfterRender
{
private bool hasCalledOnAfterRender;
[Inject]
private IServiceProvider Services { get; set; } = default!;
[Inject]
private BlazorServiceAccessor BlazorServiceAccessor { get; set; } = default!;
public override Task SetParametersAsync(ParameterView parameters)
=> InvokeWithBlazorServiceContext(() => base.SetParametersAsync(parameters));
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
=> InvokeWithBlazorServiceContext(() =>
{
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
});
Task IHandleAfterRender.OnAfterRenderAsync()
=> InvokeWithBlazorServiceContext(() =>
{
var firstRender = !hasCalledOnAfterRender;
hasCalledOnAfterRender |= true;
OnAfterRender(firstRender);
return OnAfterRenderAsync(firstRender);
});
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
try
{
await task;
}
catch
{
if (task.IsCanceled)
{
return;
}
throw;
}
StateHasChanged();
}
private async Task InvokeWithBlazorServiceContext(Func<Task> func)
{
try
{
BlazorServiceAccessor.Services = Services;
await func();
}
finally
{
BlazorServiceAccessor.Services = null;
}
}
}
CustomComponentBase
を拡張するすべてのコンポーネントでは、BlazorServiceAccessor.Services
が現在の Blazor DI スコープの IServiceProvider に自動的に設定されます。
最後に、Program.cs
で、BlazorServiceAccessor
をスコープ付きサービスとして追加します。
builder.Services.AddScoped<BlazorServiceAccessor>();
最後に、Startup.cs
の Startup.ConfigureServices
で BlazorServiceAccessor
をスコープ付きサービスとして追加します。
services.AddScoped<BlazorServiceAccessor>();