ASP.NET Core Blazor 依赖关系注入
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
作者:Rainer Stropek 和 Mike Rousos
本文介绍 Blazor 应用如何将服务注入组件。
依赖关系注入 (DI) 是一种技术,它用于访问配置在中心位置的服务:
- 可将已注册框架的服务直接注入到 Razor 组件中。
- Blazor 应用还可定义和注册自定义服务,并通过 DI 使其在整个应用中可用。
注意
在阅读本主题之前,建议先阅读 ASP.NET Core 中的依赖项注入。
默认服务
下表中所示的服务通常在 Blazor 应用中使用。
服务 | 生存期 | 描述 |
---|---|---|
HttpClient | 范围内 | 提供用于发送 HTTP 请求以及从 URI 标识的资源接收 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
文件中的应用服务集合配置服务。 在下例中,为 IExampleDependency
注册了 ExampleDependency
实现:
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)
{
...
}
向 ConfigureServices 方法传递 IServiceCollection,它是服务描述符对象列表。 通过向服务集合提供服务描述符来在 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}}.Client.Pages.{COMPONENT NAME}”的{PROPERTY}提供值。 没有“{SERVICE}”类型的已注册服务。
请使用以下任一方法解决此问题:
- 在主项目中注册该服务,使其在组件预呈现期间可用。
- 如果组件不需要预呈现,请遵循 ASP.NET Core Blazor 呈现模式中的指导禁用预呈现。 如果采用此方法,则无需在主项目中注册该服务。
有关详细信息,请参阅客户端服务在预呈现期间无法解析。
服务生存期
可使用下表中所示的生存期配置服务。
生存期 | 描述 |
---|---|
Scoped | 客户端当前没有 DI 范围的概念。 已注册 服务器端开发支持跨 HTTP 请求,但不支持跨客户端上加载的组件之间 SignalR 连接/线路消息的
有关在服务器端应用中保留用户状态的详细信息,请参阅 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 指令将一个或多个服务注入组件,该指令具有两个参数:
- 类型:要注入的服务的类型。
- 属性:接收注入的应用服务的属性的名称。 属性无需手动创建。 编译器会创建属性。
有关详细信息,请参阅 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
指令。 在以下示例中,CustomComponentBase
的所有注入的服务均可供 Demo
组件使用:
@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 范围。 使用此范围时,组件可以注入具有范围内生存期的服务,并使其生存期与组件一样长。 销毁组件时,也会处置组件的 Scoped 服务提供程序提供的服务。 这对于在组件内重复使用但不跨组件共享的服务很有用。
以下两个部分中提供并介绍了 OwningComponentBase 类型的两个版本:
OwningComponentBase
OwningComponentBase 是 ComponentBase 类型的抽象、可释放子级,其具有 IServiceProvider 类型的受保护的 ScopedServices 属性。 提供程序可用于解析范围限定为组件生存期的服务。
使用 @inject
或 [Inject]
特性注入到组件中的 DI 服务不在组件的范围内创建。 要使用组件的范围,必须使用带有 GetRequiredService 或 GetService 的 ScopedServices 解析服务。 任何使用 ScopedServices 提供程序进行解析的服务都具有组件的范围内提供的依赖关系。
以下示例演示了直接注入限定范围服务与在服务器上使用 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;
}
该服务在服务器端 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,并添加从范围内 DI 提供程序返回 T
实例的 Service 属性。 当存在一项应用需要从使用组件范围的 DI 容器中获取的主服务时,不必使用 IServiceProvider 的实例即可通过此类型便捷地访问 Scoped 服务。 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 存储库(如何下载)中获取。
检查 .NET 6 或更高版本的 BlazorSample_WebAssembly
示例中的以下内容:
DetectIncorrectUsagesOfTransientDisposables.cs
Services/TransientDisposableService.cs
- 在
Program.cs
中:- 应用的
Services
命名空间位于文件顶部 (using BlazorSample.Services;
)。 - 从 WebAssemblyHostBuilder.CreateDefault 分配
builder
后,立即调用DetectIncorrectUsageOfTransients
。 TransientDisposableService
已注册 (builder.Services.AddTransient<TransientDisposableService>();
)。- 在应用的处理管道中的已生成主机上调用
EnableTransientDisposableDetection
(host.EnableTransientDisposableDetection();
)。
- 应用的
- 应用注册
TransientDisposableService
服务而不引发异常。 但是,当框架尝试构造TransientDisposableService
的实例时,尝试解析TransientService.razor
中的服务会引发 InvalidOperationException。
检测服务器端暂时性可释放对象
可以将自定义代码添加到服务器端 Blazor 应用中,以检测应使用 OwningComponentBase 的应用中的服务器端一次性临时服务。 如果你担心将来添加到应用的代码会消耗一个或多个临时一次性服务(包括库添加的服务),则可使用此方法。 演示代码可在 Blazor 示例 GitHub 存储库(如何下载)中获取。
检查 .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>();
)。 - 为
ITransitiveTransientDisposableDependency
注册TransitiveTransientDisposableDependency
(builder.Services.AddTransient<ITransitiveTransientDisposableDependency, 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) 范围(例如使用 IHttpClientFactory 创建的范围)访问限定范围的 Blazor 服务。
在 .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
来访问线路范围内的服务。
有关演示如何使用 IHttpClientFactory 从 DelegatingHandler 设置访问 AuthenticationStateProvider 的示例,请参阅服务器端 ASP.NET CoreBlazor 其他安全方案。
有时 Razor 组件会调用在不同 DI 范围内执行代码的异步方法。 如果没有正确的方法,这些 DI 范围无法访问 Blazor 的服务,例如 IJSRuntime 和 Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage。
例如,使用 IHttpClientFactory 创建的 HttpClient 实例具有其自己的 DI 服务范围。 因此,在 HttpClient 上配置的 HttpMessageHandler 实例无法直接注入 Blazor 服务。
创建一个定义 AsyncLocal
(存储当前异步上下文的 BlazorIServiceProvider)的 BlazorServiceAccessor
类。 可以从不同的 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
的组件都自动在当前 Blazor DI 范围内将 BlazorServiceAccessor.Services
设置为 IServiceProvider。
最后,在 Program
文件中,添加 BlazorServiceAccessor
作为限定范围服务:
builder.Services.AddScoped<BlazorServiceAccessor>();
最后,在 Startup.cs
的 Startup.ConfigureServices
中,将 BlazorServiceAccessor
添加为限定范围的服务:
services.AddScoped<BlazorServiceAccessor>();