ASP.NET Core Blazor 相依性插入

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱 本文的 ASP.NET Core 8.0 版本。

作者為 Rainer StropekMike Rousos

本文說明 Blazor 應用程式可如何將服務插入至元件中。

相依性插入 (DI) 是存取設定於中央位置之服務的技術:

  • 架構註冊服務可以直接插入至 Razor 元件中。
  • Blazor 應用程式會定義與註冊自訂服務,並透過 DI 在整個應用程式中提供這些服務。

注意

在閱讀本主題之前,建議您先閱讀 ASP.NET Core 中的相依性插入

預設服務

下表所示的服務通常用於 Blazor 應用程式。

服務 存留期 描述
HttpClient 具範圍

提供從 URI 所識別的資源傳送 HTTP 要求,以及接收 HTTP 回應的方法。

用戶端為 HttpClient 的執行個體,是由 Program 檔案中的應用程式註冊,並使用瀏覽器來處理背景中的 HTTP 流量。

伺服器端為 HttpClient,預設不會設定為服務。 在伺服器端程式碼中提供 HttpClient

如需詳細資訊,請參閱從 ASP.NET Core Blazor 應用程式呼叫 Web API

HttpClient 是註冊為範圍性服務,而非單一資料庫。 如需詳細資訊,請參閱服務存留期 (部分機器翻譯) 一節。

IJSRuntime

用戶端:單一資料庫

伺服器端:範圍性

Blazor 架構會在應用程式的服務容器中註冊 IJSRuntime

表示 JavaScript 執行階段的執行個體,JavaScript 呼叫會在其中分配。 如需詳細資訊,請參閱從 ASP.NET Core Blazor 中的 .NET 方法呼叫 JavaScript 函式

當試圖將服務插入伺服器上的單一資料庫服務時,請採取下列任一方法:

  • 將服務註冊變更為範圍性,以符合 IJSRuntime 的註冊 (如果服務處理使用者特定狀態,則適用)。
  • IJSRuntime 傳遞至單一資料庫服務的實作,以做為其方法呼叫的引數,而非將其插入至單一資料庫中。
NavigationManager

用戶端:單一資料庫

伺服器端:範圍性

Blazor 架構會在應用程式的服務容器中註冊 NavigationManager

包含使用 URI 和導覽狀態的協助程式。 如需詳細資訊,請參閱 URI 和導覽狀態的協助程式 (部分機器翻譯)。

Blazor 架構所註冊的其他服務會在文件中說明 (文件中它們會用來描述 Blazor 功能,例如設定和記錄)。

自訂服務提供者不會自動提供資料表中列出的預設服務。 如果您是使用自訂服務提供者,且需要任何顯示在資料表中的服務,請將必要的服務新增至新增服務提供者。

新增用戶端服務

Program 檔案中設定應用程式服務集合的服務。 在下列範例中,ExampleDependency 實作已註冊 IExampleDependency

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

await builder.Build().RunAsync();

建置主機後,服務可以在轉譯任何元件之前,從根 DI 範圍取得。 這對於在轉譯內容之前執行初始化邏輯非常有用:

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

var host = builder.Build();

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

await host.RunAsync();

主機會為應用程式提供集中設定執行個體。 根據上述範例,天氣服務的 URL 會從預設設定來源 (例如 appsettings.json) 傳遞至 InitializeWeatherAsync

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

var host = builder.Build();

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

await host.RunAsync();

新增伺服器端服務

建立新的應用程式之後,請檢查 Program 檔案的一部分:

var builder = WebApplication.CreateBuilder(args);

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

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

builder 變數表示具有 IServiceCollectionWebApplicationBuilder (此為服務描述項物件的清單)。 服務會藉由將服務描述項提供至服務集合來新增。 下列範例示範了IDataAccess 介面及其具體實作 DataAccess 的概念:

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

建立新的應用程式之後,請檢查 Startup.cs 中的 Startup.ConfigureServices 方法:

using Microsoft.Extensions.DependencyInjection;

...

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

系統將 IServiceCollection (此為服務描述項物件的清單) 傳遞給 ConfigureServices 方法。 服務會藉由將服務描述項提供至服務集合,以在 ConfigureServices 方法中新增。 下列範例示範了IDataAccess 介面及其具體實作 DataAccess 的概念:

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

註冊常用服務

如果用戶端和伺服器端都需要一或多個常用服務,您可以將常用服務註冊置放於方法用戶端中,並呼叫方法以在兩項專案中註冊該服務。

首先,將常用服務註冊分解為個別的方法。 例如,建立 ConfigureCommonServices 方法用戶端:

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

針對用戶端 Program 檔案,呼叫 ConfigureCommonServices 以註冊常用服務:

var builder = WebAssemblyHostBuilder.CreateDefault(args);

...

ConfigureCommonServices(builder.Services);

在伺服器端 Program 檔案中,呼叫 ConfigureCommonServices 以註冊常用服務:

var builder = WebApplication.CreateBuilder(args);

...

Client.Program.ConfigureCommonServices(builder.Services);

如需此方法的範例,請參閱 ASP.NET Core Blazor WebAssembly 其他安全性情節 (部分機器翻譯)。

在預先呈現期間失敗的客戶端服務

本節僅適用於 Web Apps 中的 Blazor WebAssembly 元件。

Blazor Web Apps 通常會預先呈現用戶端 WebAssembly 元件。 如果應用程式是在專案中只註冊 .Client 的必要服務執行,當元件在預先呈現期間嘗試使用必要服務時,執行應用程式會產生類似下列的運行時間錯誤:

InvalidOperationException: 無法在類型 '{ASSEMBLY}} 上提供 {PROPERTY} 的值。Client.Pages。{COMPONENT NAME}'。 沒有類型為 '{SERVICE}' 的已註冊服務。

使用 下列任一 方法來解決此問題:

  • 在主要專案中註冊服務,使其在元件預先呈現期間可供使用。
  • 如果元件不需要預先呈現,請遵循 ASP.NET Core Blazor 轉譯模式中的指引來停用預先呈現。 如果您採用此方法,則不需要在主要專案中註冊服務。

如需詳細資訊,請參閱 用戶端服務無法在預先呈現期間解決。

服務存留期

服務可以使用下表所示的存留期進行設定。

存留期 描述
Scoped

用戶端目前沒有 DI 範圍的概念。 Scoped 註冊服務的行為類似 Singleton 服務。

伺服器端開發支援跨 HTTP 要求的 Scoped 存留期,但不支援在用戶端上載入之元件間的跨 SignalR 連線/線路訊息。 應用程式的 Razor Pages 或 MVC 部分會正常處理範圍服務,並在頁面或檢視間瀏覽或從頁面或檢視瀏覽至元件時,在每個 HTTP 要求上重新建立該服務。 在用戶端上的元件間瀏覽時不會重建範圍服務,其中與伺服器的通訊是透過使用者線路的 SignalR 連線進行,而不是透過 HTTP 要求。 在以下的用戶端上元件情節中會重建範圍服務,因為會為使用者建立新的線路:

  • 使用者關閉瀏覽器的視窗。 使用者開啟新的視窗,並瀏覽回到應用程式。
  • 使用者在瀏覽器視窗中關閉應用程式的分頁。 使用者開啟新的分頁,並瀏覽回到應用程式。
  • 使用者選取瀏覽器的重新載入/重新整理按鈕。

如需在伺服器端應用程式中保留使用者狀態的詳細資訊,請參閱 ASP.NET Core Blazor 狀態管理

Singleton DI 會建立服務的單一執行個體。 要求 Singleton 服務的所有元件都會收到相同的服務執行個體。
Transient 每當元件從服務容器取得 Transient 服務的執行個體時,就會收到該服務的新增執行個體

DI 系統是以 ASP.NET Core 中的 DI 系統為基礎。 如需詳細資訊,請參閱在 ASP.NET Core 中插入相依性

要求元件中的服務

若要將服務插入元件, Blazor 支援 建構函式插入屬性插入

建構函式插入

將服務新增至服務集合之後,請使用建構函式插入將一或多個服務插入元件。 下列範例會 NavigationManager 插入服務。

ConstructorInjection.razor

@page "/constructor-injection"

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

ConstructorInjection.razor.cs

using Microsoft.AspNetCore.Components;

public partial class ConstructorInjection(NavigationManager navigation)
{
    protected NavigationManager Navigation { get; } = navigation;
}

屬性插入

將服務新增至服務集合之後,請使用 具有兩個參數的 @injectRazor 指示詞,將一或多個服務插入元件:

  • 型別:要插入的服務型別。
  • 屬性:接收插入 App Service 的屬性名稱。 屬性不需要手動建立。 編譯器會建立屬性。

如需詳細資訊,請參閱 ASP.NET Core 中檢視的相依性插入 (部分機器翻譯)。

使用多個 @inject 陳述式來插入不同的服務。

下列範例示範如何使用 @inject 指示詞。 實作 Services.NavigationManager 的服務會插入至元件的屬性 Navigation 中。 請注意程式代碼只使用 NavigationManager 抽象概念的方式。

PropertyInjection.razor

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

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

在內部,產生的屬性 (Navigation) 會使用 [Inject] 屬性。 通常不會直接使用這個屬性。 如果元件需要基底類別,而基底類別也需要插入的屬性,請手動新增 [Inject] 屬性

using Microsoft.AspNetCore.Components;

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

    ...
}

注意

由於預期插入的服務會可供使用,因此在 .NET 6 或更新版本中會指派具有 Null 放棄運算子 (default!) 的預設常值。 如需詳細資訊,請參閱可為 Null 的參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析

在衍生自基類的元件中,不需要指示 @inject 詞。 InjectAttribute基類的 已足夠。 元件只需要 指示 @inherits 詞。 在下列範例中,元件可以使用 Demo 的任何插入服務CustomComponentBase

@page "/demo"
@inherits CustomComponentBase

使用服務中的 DI

複雜服務可能需要額外的服務。 在下列範例中,DataAccess 需要 HttpClient 預設服務。 @inject (或 [Inject] 屬性) 不可在服務中使用。 必須改為使用建構函式插入。 必要服務會藉由將參數加入至服務的建構函式來新增。 當 DI 建立服務時,它會辨識建構函式中所需的服務,並據此加以提供。 在下列範例中,建構函式會透過 DI 接收 HttpClientHttpClient 是預設服務。

using System.Net.Http;

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

建構函式插入的必要條件:

  • 一個建構函式必須存在,其引數都可以由 DI 實現。 如果 DI 未涵蓋的其他參數指定了預設值,則可允許。
  • 適用的建構函式必須是 public
  • 必須存在一項適用的建構函式。 如果模棱兩可,DI 會擲回例外狀況。

將索引鍵服務插入元件中

Blazor 支援使用 [Inject] 屬性插入索引鍵服務。 索引鍵允許在使用相依性插入時,限制服務的註冊和取用範圍。 InjectAttribute.Key使用 屬性來指定服務要插入的索引鍵:

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

用來管理 DI 範圍的公用程式基底元件類別

在非Blazor ASP.NET 核心應用程式中,範圍和暫時性服務通常會限定為目前的要求。 要求完成之後,DI 系統會處置範圍和暫時性服務。

在互動式伺服器端 Blazor 應用程式中,DI 範圍會持續到線路期間( SignalR 用戶端與伺服器之間的連線),這可能會導致範圍和可處置的暫時性服務壽命比單一元件的存留期長得多。 因此,如果您想要讓服務存留期符合元件的存留期,請勿直接將範圍服務插入元件中。 插入至未實 IDisposable 作的元件暫時性服務會在處置元件時進行垃圾收集。 不過,實作 的插入暫時性服務IDisposable會在線路的存留期內由 DI 容器維護,這樣會在處置元件並導致記憶體流失時,防止服務垃圾收集。 本節稍後會說明以型別為基礎的 OwningComponentBase 範圍服務替代方法,不應完全使用可處置的暫時性服務。 如需詳細資訊,請參閱設計來解決 (dotnet/aspnetcore#26676) 上的Blazor Server暫時性可處置專案。

即使在未透過線路運作的用戶端 Blazor 應用程式中,以限定存留期註冊的服務也會被視為單一實例,因此它們比一般 ASP.NET Core 應用程式中的範圍服務更久。 用戶端可處置暫時性服務也比插入的元件壽命還長,因為保存可處置服務的參考DI容器會保存應用程式存留期,因而防止服務上的垃圾收集。 雖然伺服器上的長期可處置暫時性服務更關心,但也應該避免做為客戶端服務註冊。 OwningComponentBase也建議針對用戶端範圍服務使用 型別來控制服務存留期,而且完全不應該使用可處置暫時性服務。

限制服務存留期的方法是使用 OwningComponentBase 型別。 OwningComponentBase 是衍生自 ComponentBase 的抽象型別,會建立對應至 元件存留期的 DI 範圍。 使用此範圍,元件可以插入具有範圍存留期的服務,並讓它們存留期,只要元件即可存留。 當元件終結時,也會處置來自元件範圍服務提供者的服務。 這對於元件內重複使用但未跨元件共用的服務很有用。

有兩種 OwningComponentBase 型別的版本可供使用,會在接下來的兩節中說明:

OwningComponentBase

OwningComponentBase 是抽象可處置的 ComponentBase 型別子項目,具有型別 IServiceProvider 的受保護 ScopedServices 屬性。 可使用提供者來解析範圍設定為元件存留期的服務。

使用 @inject[Inject] 屬性 插入元件中的 DI 服務不會在元件的範圍中建立。 若要使用元件的範圍,必須搭配 GetRequiredServiceGetService 使用 ScopedServices 來解析服務。 使用 ScopedServices 提供者解析的任何服務,其相依性會於元件範圍中提供。

下列範例示範直接插入範圍服務,以及在伺服器上使用 ScopedServices 解析服務之間的不同。 下列時間移動類別的介面和實作包含了用來保存 DateTime 值的 DT 屬性。 實作會呼叫 DateTime.Now,以在具現化 TimeTravel 類別時設定 DT

ITimeTravel.cs

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

TimeTravel.cs

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

服務會在伺服器端 Program 檔案中註冊為範圍。 伺服器端,範圍服務存留期等於線路的持續時間。

Program 檔案中:

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

在下列 TimeTravel 元件中:

  • 時間移動服務會直接以 @inject 插入以作為 TimeTravel1
  • 服務也會以 ScopedServicesGetRequiredService 個別解析以作為 TimeTravel2

TimeTravel.razor

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

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

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

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

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

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

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

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

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

一開始瀏覽至 TimeTravel 元件時,時間移動服務會在元件載入時遭具現化兩次,而且 TimeTravel1TimeTravel2 會具有相同的初始值:

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

當從 TimeTravel 元件瀏覽至另一個元件並回到 TimeTravel 元件時:

  • 系統會提供TimeTravel1 第一次載入元件時所建立的相同服務執行個體,因此 DT 的值會維持不變。
  • TimeTravel2 使用新的 DT 值,在 TimeTravel2 中取得新的 ITimeTravel 服務執行個體。

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

TimeTravel1 繫結至使用者的線路,該線路會保持不變,而且在解構基礎線路之前不會加以處置。 例如,如果線路在中斷連線的線路保留期間中斷連線,則會處置該服務。

儘管 Program 檔案中的範圍服務註冊和使用者線路壽命,每次具現化元件時,TimeTravel2 都會收到新的 ITimeTravel 服務執行個體。

OwningComponentBase<TService>

OwningComponentBase<TService> 衍生自 OwningComponentBase 且新增 Service 屬性,會從範圍 DI 提供者傳回T 的執行個體。 當應用程式需要使用元件範圍從 DI 容器取得一個主要服務時,此型別是存取範圍服務的便捷方法,無需使用 IServiceProvider 的執行個體。 ScopedServices 屬性可供使用,因此應用程式可以視需要取得其他型別的服務。

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

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

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

偵測用戶端暫時性可處置項

自定義程式代碼可以新增至用戶端應用程式 Blazor ,以偵測應使用 OwningComponentBase之應用程式中的可處置暫時性服務。 如果您擔心未來新增至應用程式的程式代碼會耗用一或多個暫時性可處置服務,包括連結庫新增的服務,此方法會很有用。 示範程式代碼可在 GitHub 存放庫範例中Blazor取得(如何下載)。

在 .NET 6 或更新版本中 BlazorSample_WebAssembly 檢查下列範例:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransientDisposableService.cs
  • Program.cs中:
    • 應用程式的 Services 命名空間會提供於檔案頂端 (using BlazorSample.Services;)。
    • DetectIncorrectUsageOfTransients會在 從 WebAssemblyHostBuilder.CreateDefault指定之後builder立即呼叫 。
    • TransientDisposableService已註冊 (builder.Services.AddTransient<TransientDisposableService>();)。
    • EnableTransientDisposableDetection在應用程式的處理管線中,於內建主機上呼叫 。host.EnableTransientDisposableDetection();
  • 應用程式會註冊服務, TransientDisposableService 而不擲回例外狀況。 不過,當架構嘗試建構 實例TransientDisposableService時,嘗試解析 中的TransientService.razor服務會擲回 InvalidOperationException

偵測伺服器端暫時性可處置項

自定義程式代碼可以新增至伺服器端應用程式,以偵測應該使用 OwningComponentBase之應用程式中的伺服器端Blazor可處置暫時性服務。 如果您擔心未來新增至應用程式的程式代碼會耗用一或多個暫時性可處置服務,包括連結庫新增的服務,此方法會很有用。 示範程式代碼可在 GitHub 存放庫範例中Blazor取得(如何下載)。

在 .NET 8 或更新版本中 BlazorSample_BlazorWebApp 檢查下列範例:

在範例的 .NET 6 或 .NET 7 版本中 BlazorSample_Server 檢查下列專案:

  • DetectIncorrectUsagesOfTransientDisposables.cs
  • Services/TransitiveTransientDisposableDependency.cs
  • Program.cs中:
    • 應用程式的 Services 命名空間會提供於檔案頂端 (using BlazorSample.Services;)。
    • DetectIncorrectUsageOfTransients 在主機產生器上呼叫 (builder.DetectIncorrectUsageOfTransients();)。
    • 服務 TransientDependency 已註冊 (builder.Services.AddTransient<TransientDependency>();)。
    • TransitiveTransientDisposableDependency已註冊 ITransitiveTransientDisposableDependencybuilder.Services.AddTransient<ITransitiveTransientDisposableDependency, TransitiveTransientDisposableDependency>();)。
  • 應用程式會註冊服務, TransientDependency 而不擲回例外狀況。 不過,當架構嘗試建構 實例TransientDependency時,嘗試解析 中的TransientService.razor服務會擲回 InvalidOperationException

處理程式的 IHttpClientFactory/HttpClient 暫時性服務註冊

建議使用 IHttpClientFactory/HttpClient 處理常式的暫時性服務註冊。 如果應用程式包含 IHttpClientFactory/HttpClient 處理程式並使用 IRemoteAuthenticationBuilder<TRemoteAuthenticationState,TAccount> 來新增驗證支援,也會探索客戶端驗證的下列暫時性可處置專案,這是預期且可以忽略的:

也會探索的其他 實例 IHttpClientFactory/HttpClient 。 您也可以忽略這些實例。

GitHub 存放Blazor庫範例中的Blazor範例應用程式(如何下載)示範程序代碼,以偵測暫時性可處置專案。 不過,因為範例應用程式包含 IHttpClientFactory/HttpClient 處理程式,因此會停用程式代碼。

若要啟用示範程式碼並見證其作業:

  • 取消批注 中的 Program.cs暫時性可處置行。

  • 移除條件式簽入 NavLink.razor ,以防止 TransientService 元件顯示在應用程式的導覽提要欄中:

    - else if (name != "TransientService")
    + else
    
  • 執行範例應用程式,並流覽至 位於/transient-serviceTransientService元件。

使用來自從 DI 的 Entity Framework Core (EF Core) DbContext

如需詳細資訊,請參閱 使用 Entity Framework Core (EF Core) 的 ASP.NET Core Blazor

從不同的 DI 範圍存取伺服器端 Blazor 服務

線路活動處理常式提供了從其他非 Blazor 相依性插入 (DI) 範圍存取範圍 Blazor 服務的方法,例如使用 IHttpClientFactory 建立的範圍。

在 .NET 8 中發行 ASP.NET Core 之前,請使用自定義基底元件類型,從其他相依性插入範圍存取線路範圍服務。 使用線路活動處理常式時,不需要自訂基底元件型別,如下列範例所示:

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

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

public class ServicesAccessorCircuitHandler : 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 其他安全性情節

Razor 元件有時會叫用非同步方法,以在不同的 DI 範圍中執行程式碼。 如果沒有正確的方法,這些 DI 範圍就無法存取 Blazor 的服務,例如 IJSRuntimeMicrosoft.AspNetCore.Components.Server.ProtectedBrowserStorage

例如,使用 IHttpClientFactory 建立的 HttpClient 執行個體有自己的 DI 服務範圍。 因此,HttpClient 上設定的 HttpMessageHandler 執行個體就無法直接插入 Blazor 服務。

建立定義 AsyncLocal 的類別 BlazorServiceAccessor,其會為目前非同步內容儲存 BlazorIServiceProvider。 您可以從不同的 DI 服務範圍中取得 BlazorServiceAcccessor 執行個體,以存取 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 元件程式碼中:

下列類別示範基底元件的實作。

CustomComponentBase.cs

using Microsoft.AspNetCore.Components;

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

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

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

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

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

            StateHasChanged();

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

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

            OnAfterRender(firstRender);

            return OnAfterRenderAsync(firstRender);
        });

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

            throw;
        }

        StateHasChanged();
    }

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

任何自動擴充 CustomComponentBase 的元件都會將 BlazorServiceAccessor.Services 設定為目前 Blazor DI 範圍中的 IServiceProvider

最後,在 Program 檔案中,將 BlazorServiceAccessor 新增作為範圍服務:

builder.Services.AddScoped<BlazorServiceAccessor>();

最後,在 Startup.csStartup.ConfigureServices 中,將 BlazorServiceAccessor 新增作為範圍服務:

services.AddScoped<BlazorServiceAccessor>();

其他資源