共用方式為


ASP.NET Core Blazor 相依性插入

注意

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

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

作者為 Rainer StropekMike Rousos

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

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

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

注意

在閱讀本主題之前,建議您先閱讀 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 其他安全性情節 (部分機器翻譯)。

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

本節僅適用於 Blazor Web Apps 中的 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 Core 應用程式中,通常將有限範圍和暫時性服務的範圍限定為目前的要求。 在要求完成之後,DI 系統會處置任何有限範圍和暫時性服務。

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

即使在未透過線路運作的用戶端 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 之應用程式中的可處置暫時性服務。 如果您擔心未來新增至應用程式的程式碼會耗用一或多個暫時性可處置服務 (包括程式庫新增的服務),則此方法很有用。 您可在 Blazor GitHub 存放庫範例 (如何下載) 中取得示範程式碼。

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

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

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

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

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

BlazorSample_Server 範例的 .NET 6 或 .NET 7 版本中檢查下列事項:

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

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

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

也會探索其他的 IHttpClientFactory/HttpClient 執行個體。 您也可以忽略這些執行個體。

Blazor GitHub 存放庫範例 (如何下載) 中的 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>();

其他資源