ASP.NET Core Blazor 相依性插入
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
作者為 Rainer Stropek 與 Mike Rousos
本文說明 Blazor 應用程式可如何將服務插入至元件中。
相依性插入 (DI) 是存取設定於中央位置之服務的技術:
- 架構註冊服務可以直接插入至 Razor 元件中。
- Blazor 應用程式會定義與註冊自訂服務,並透過 DI 在整個應用程式中提供這些服務。
注意
在閱讀本主題之前,建議您先閱讀 ASP.NET Core 中的相依性插入。
預設服務
下表所示的服務通常用於 Blazor 應用程式。
服務 | 存留期 | 描述 |
---|---|---|
HttpClient | 具範圍 | 提供從 URI 所識別的資源傳送 HTTP 要求,以及接收 HTTP 回應的方法。 用戶端為 HttpClient 的執行個體,是由 伺服器端為 HttpClient,預設不會設定為服務。 在伺服器端程式碼中提供 HttpClient。 如需詳細資訊,請參閱從 ASP.NET Core Blazor 應用程式呼叫 Web API HttpClient 是註冊為範圍性服務,而非單一資料庫。 如需詳細資訊,請參閱服務存留期 (部分機器翻譯) 一節。 |
IJSRuntime | 用戶端:單一資料庫 伺服器端:範圍性 Blazor 架構會在應用程式的服務容器中註冊 IJSRuntime。 |
表示 JavaScript 執行階段的執行個體,JavaScript 呼叫會在其中分配。 如需詳細資訊,請參閱從 ASP.NET Core Blazor 中的 .NET 方法呼叫 JavaScript 函式。 當試圖將服務插入伺服器上的單一資料庫服務時,請採取下列任一方法:
|
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
變數表示具有 IServiceCollection 的 WebApplicationBuilder (此為服務描述項物件的清單)。 服務會藉由將服務描述項提供至服務集合來新增。 下列範例示範了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 App 中的 WebAssembly 元件。
Blazor Web App 通常會先轉譯用戶端 WebAssembly 元件。 如果應用程式是透過只在 .Client
專案中註冊的必要服務加以執行,當元件在預先轉譯期間嘗試使用必要服務時,執行應用程式會產生類似下列的執行階段錯誤:
InvalidOperationException:無法為類型為 '{ASSEMBLY}} 的 {PROPERTY} 提供值。Client.Pages.{COMPONENT NAME}'。 沒有類型為 '{SERVICE}' 的已註冊服務。
若要解決此問題,請使用下列任一種方法:
- 在主要專案中註冊服務,使其在元件預先轉譯期間可供使用。
- 如果元件不需要預先轉譯,請遵循 ASP.NET Core Blazor 轉譯模式中的指導,來停用預先轉譯。 如果您採用此方法,則不需要在主要專案中註冊此服務。
如需詳細資訊,請參閱用戶端服務無法在預先轉譯期間解析。
服務存留期
服務可以使用下表所示的存留期進行設定。
存留期 | 描述 |
---|---|
Scoped | 用戶端目前沒有 DI 範圍的概念。 伺服器端開發支援跨 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="HandleClick">
Take me to the Counter component
</button>
ConstructorInjection.razor.cs
:
using Microsoft.AspNetCore.Components;
public partial class ConstructorInjection(NavigationManager navigation)
{
private void HandleClick()
{
navigation.NavigateTo("/counter");
}
}
屬性插入
將服務新增至服務集合之後,請使用 @inject
Razor 指示詞 (包含兩個參數),將一或多個服務插入至元件:
- 型別:要插入的服務型別。
- 屬性:接收插入 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 接收 HttpClient。 HttpClient 是預設服務。
using System.Net.Http;
public class DataAccess : IDataAccess
{
public DataAccess(HttpClient http)
{
...
}
...
}
C# 12 (.NET 8) 或更新版本中的 主要建構函式 有支援建構函式插入:
using System.Net.Http;
public class DataAccess(HttpClient http) : IDataAccess
{
...
}
建構函式插入的必要條件:
- 一個建構函式必須存在,其引數都可以由 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 服務不會在元件的範圍中建立。 若要使用元件的範圍,必須搭配 GetRequiredService 或 GetService 使用 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
。 - 服務也會以 ScopedServices 和 GetRequiredService 個別解析以作為
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
元件時,時間移動服務會在元件載入時遭具現化兩次,而且 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
檔案中的範圍服務註冊和使用者線路壽命,每次具現化元件時,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-service
的TransientService
元件。
使用來自從 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(
IServiceProvider services, CircuitServicesAccessor servicesAccessor)
: CircuitHandler
{
public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next) =>
async context =>
{
servicesAccessor.Services = services;
await next(context);
servicesAccessor.Services = null;
};
}
public static class CircuitServicesServiceCollectionExtensions
{
public static IServiceCollection AddCircuitServicesAccessor(
this IServiceCollection services)
{
services.AddScoped<CircuitServicesAccessor>();
services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();
return services;
}
}
插入所需的 CircuitServicesAccessor
,以存取線路範圍服務。
如需示範如何使用 從 設定存取 AuthenticationStateProvider 的範例,請參閱 ASP.NET Core 伺服器端和其他Blazor Web App安全性案例。DelegatingHandler IHttpClientFactory
Razor 元件有時會叫用非同步方法,以在不同的 DI 範圍中執行程式碼。 如果沒有正確的方法,這些 DI 範圍就無法存取 Blazor 的服務,例如 IJSRuntime 和 Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage。
例如,使用 IHttpClientFactory 建立的 HttpClient 執行個體有自己的 DI 服務範圍。 因此,HttpClient 上設定的 HttpMessageHandler 執行個體就無法直接插入 Blazor 服務。
建立定義 AsyncLocal
的類別 BlazorServiceAccessor
,其會為目前非同步內容儲存 BlazorIServiceProvider。 您可以從不同的 DI 服務範圍中取得 BlazorServiceAccessor
執行個體,以存取 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.cs
的 Startup.ConfigureServices
中,將 BlazorServiceAccessor
新增作為範圍服務:
services.AddScoped<BlazorServiceAccessor>();