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 范围内

提供用于发送 HTTP 请求以及从 URI 标识的资源接收 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 文件中的应用服务集合配置服务。 在下例中,为 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 变量表示带有 IServiceCollectionWebApplicationBuilder,它是服务描述符对象列表。 通过向服务集合提供服务描述符来添加服务。 下面的示例演示了 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 范围的概念。 已注册 Scoped 的服务的行为与 Singleton 服务类似。

服务器端开发支持跨 HTTP 请求,但不支持跨客户端上加载的组件之间 SignalR 连接/线路消息的 Scoped 生命周期。 在页面或视图之间或从页面或视图导航到组件时,应用的 Razor 页面或 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="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");
    }
}

属性注入

将服务添加到服务集合后,使用 @injectRazor 指令将一个或多个服务注入组件,该指令具有两个参数:

  • 类型:要注入的服务的类型。
  • 属性:接收注入的应用服务的属性的名称。 属性无需手动创建。 编译器会创建属性。

有关详细信息,请参阅 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 接收 HttpClientHttpClient 是默认服务。

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

OwningComponentBaseComponentBase 类型的抽象、可释放子级,其具有 IServiceProvider 类型的受保护的 ScopedServices 属性。 提供程序可用于解析范围限定为组件生存期的服务。

使用 @inject[Inject] 特性注入到组件中的 DI 服务不在组件的范围内创建。 要使用组件的范围,必须使用带有 GetRequiredServiceGetServiceScopedServices 解析服务。 任何使用 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
  • 该服务还使用 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,并添加从范围内 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 来访问线路范围内的服务。

有关演示如何使用 IHttpClientFactoryDelegatingHandler 设置访问 AuthenticationStateProvider 的示例,请参阅服务器端 ASP.NET CoreBlazor 其他安全方案

有时 Razor 组件会调用在不同 DI 范围内执行代码的异步方法。 如果没有正确的方法,这些 DI 范围无法访问 Blazor 的服务,例如 IJSRuntimeMicrosoft.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.csStartup.ConfigureServices 中,将 BlazorServiceAccessor 添加为限定范围的服务:

services.AddScoped<BlazorServiceAccessor>();

其他资源