ASP.NET Core Blazor 依赖关系注入ASP.NET Core Blazor dependency injection

本文内容

作者:Rainer StropekMike Rousos

重要

Blazor WebAssembly 为预览版状态

ASP.NET Core 3.0 支持 Blazor Server。Blazor WebAssembly 在 ASP.NET Core 3.1 中为预览版。

Blazor 支持依赖关系注入 (DI)应用可通过将内置服务注入组件来使用这些服务。应用还可定义和注册自定义服务,并通过 DI 使其在整个应用中可用。

DI 是一种技术,它用于访问配置在中心位置的服务。该技术可在 Blazor 应用中用于以下方面:

  • 跨多个组件共享服务类的单个实例,称为“单一实例”服务 。
  • 通过使用引用抽象将组件与具体服务类分离。例如,请考虑用接口 IDataAccess 来访问应用中数据。该接口由具体的 DataAccess 类实现,并在应用的服务容器中注册为服务。当组件使用 DI 接收 IDataAccess 实现时,组件不与具体类型耦合。可交换实现,目的可能是为了单元测试中的模拟实现。

默认服务Default services

默认服务会自动添加到应用的服务集合中。

服务生存期描述
HttpClientSingleton提供用于发送 HTTP 请求以及从 URI 标识的资源接收 HTTP 响应的方法。Blazor WebAssembly 应用中 HttpClient 的实例使用浏览器在后台处理 HTTP 流量。默认情况下,Blazor 服务器应用不包含配置为服务的 HttpClient向 Blazor 服务器应用提供 HttpClient有关详细信息,请参阅 从 ASP.NET Core Blazor 调用 Web API
IJSRuntimeSingleton (Blazor WebAssembly)Scoped(Blazor 服务器)表示在其中调度 JavaScript 调用的 JavaScript 运行时实例。有关详细信息,请参阅 在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数
NavigationManagerSingleton (Blazor WebAssembly)Scoped(Blazor 服务器)包含用于处理 URI 和导航状态的帮助程序。有关详细信息,请参阅 URI 和导航状态帮助程序

自定义服务提供程序不会自动提供表中列出的默认服务。如果你使用自定义服务提供程序且需要表中所示的任何服务,请将所需服务添加到新的服务提供程序。

向应用添加服务Add services to an app

Blazor WebAssemblyBlazor WebAssembly

在 Program.cs 的 Main 方法中配置应用服务集合的服务 。在下例中,为 IMyDependency 注册了 MyDependency 实现:

  1. public class Program
  2. {
  3. public static async Task Main(string[] args)
  4. {
  5. var builder = WebAssemblyHostBuilder.CreateDefault(args);
  6. builder.Services.AddSingleton<IMyDependency, MyDependency>();
  7. builder.RootComponents.Add<App>("app");
  8. await builder.Build().RunAsync();
  9. }
  10. }

生成主机后,可在呈现任何组件之前从根 DI 范围访问服务。这对于在呈现内容之前运行初始化逻辑而言很有用:

  1. public class Program
  2. {
  3. public static async Task Main(string[] args)
  4. {
  5. var builder = WebAssemblyHostBuilder.CreateDefault(args);
  6. builder.Services.AddSingleton<WeatherService>();
  7. builder.RootComponents.Add<App>("app");
  8. var host = builder.Build();
  9. var weatherService = host.Services.GetRequiredService<WeatherService>();
  10. await weatherService.InitializeWeatherAsync();
  11. await host.RunAsync();
  12. }
  13. }

主机还会为应用提供中心配置实例。在上述示例的基础上,天气服务的 URL 将从默认配置源(例如 appsettings.json)传递到 InitializeWeatherAsync

  1. public class Program
  2. {
  3. public static async Task Main(string[] args)
  4. {
  5. var builder = WebAssemblyHostBuilder.CreateDefault(args);
  6. builder.Services.AddSingleton<WeatherService>();
  7. builder.RootComponents.Add<App>("app");
  8. var host = builder.Build();
  9. var weatherService = host.Services.GetRequiredService<WeatherService>();
  10. await weatherService.InitializeWeatherAsync(
  11. host.Configuration["WeatherServiceUrl"]);
  12. await host.RunAsync();
  13. }
  14. }

Blazor 服务器Blazor Server

创建新应用后,请检查 Startup.ConfigureServices 方法:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. // Add custom services here
  4. }

ConfigureServices 方法传递 IServiceCollection,它是服务描述符对象 (ServiceDescriptor) 的列表。通过向服务集合提供服务描述符来添加服务。下面的示例演示了 IDataAccess 接口的概念及其具体实现 DataAccess

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

服务生存期Service lifetime

可使用下表中所示的生存期配置服务。

生存期描述
ScopedBlazor WebAssembly 应用当前没有 DI 范围的概念。已注册 Scoped 的服务的行为与 Singleton 服务类似。但是,Blazor 服务器托管模型支持 Scoped 生存期。在 Blazor 服务器应用中,Scoped 服务注册的范围为“连接” 。因此,即使当前意图是在浏览器中运行客户端,对于范围应限定为当前用户的服务来说,首选使用 Scoped 服务。
SingletonDI 创建服务的单个实例 。需要 Singleton 服务的所有组件都会接收同一服务的实例。
Transient每当组件从服务容器获取 Transient 服务的实例时,它都会接收该服务的新实例 。

DI 系统基于 ASP.NET Core 中的 DI 系统。有关详细信息,请参阅 在 ASP.NET Core 依赖注入

在组件中请求服务Request a service in a component

将服务添加到服务集合后,使用 Razor 指令 @inject 将服务注入组件。@inject 具有两个参数:

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

有关详细信息,请参阅 在 ASP.NET Core 中将依赖项注入到视图

使用多个 @inject 语句注入不同的服务。

下面的示例说明如何使用 @inject将实现 Services.IDataAccess 的服务注入组件的 DataRepository 属性中。请注意代码是如何仅使用 IDataAccess 抽象的:

  1. @page "/customer-list"
  2. @using Services
  3. @inject IDataAccess DataRepository
  4. @if (_customers != null)
  5. {
  6. <ul>
  7. @foreach (var customer in _customers)
  8. {
  9. <li>@customer.FirstName @customer.LastName</li>
  10. }
  11. </ul>
  12. }
  13. @code {
  14. private IReadOnlyList<Customer> _customers;
  15. protected override async Task OnInitializedAsync()
  16. {
  17. _customers = await DataRepository.GetAllCustomersAsync();
  18. }
  19. }

在内部,生成的属性 (DataRepository) 使用 InjectAttribute 特性。通常,不直接使用此特性。如果组件需要基类,而基类也需要注入的属性,则请手动添加 InjectAttribute

  1. public class ComponentBase : IComponent
  2. {
  3. // DI works even if using the InjectAttribute in a component's base class.
  4. [Inject]
  5. protected IDataAccess DataRepository { get; set; }
  6. ...
  7. }

在从基类派生得到的组件中,不需要 @inject 指令。基类的 InjectAttribute 就已足够:

  1. @page "/demo"
  2. @inherits ComponentBase
  3. <h1>Demo Component</h1>

在服务中使用 DIUse DI in services

复杂的服务可能需要其他服务。在前面的示例中,DataAccess 可能需要 HttpClient 默认服务。@inject(或 InjectAttribute)不可用于服务。必须改用构造函数注入 。通过向服务的构造函数添加参数来添加所需服务。当 DI 创建服务时,它会在构造函数中识别其所需的服务,并相应地提供这些服务。

  1. public class DataAccess : IDataAccess
  2. {
  3. // The constructor receives an HttpClient via dependency
  4. // injection. HttpClient is a default service.
  5. public DataAccess(HttpClient client)
  6. {
  7. ...
  8. }
  9. }

构造函数注入的先决条件:

  • 必须存在一个构造函数,其参数可完全通过 DI 实现。如果指定默认值,则允许使用 DI 未涵盖的其他参数。
  • 适用的构造函数必须是公共函数 。
  • 必须存在一个适用的构造函数。如果出现歧义,DI 会引发异常。

用于管理 DI 范围的实用工具基组件类Utility base component classes to manage a DI scope

在 ASP.NET Core 应用中,Scoped 服务的范围通常限定为当前请求。请求完成后,DI 系统将处置所有 Scoped 或 Transient 服务。在 Blazor 服务器应用中,请求范围会在客户端连接期间一直持续,这可能导致 Transient 和 Scoped 服务的生存期比预期要长得多。在 Blazor WebAssembly 应用中,已注册 Scoped 生存期的服务被视为单一实例,因此它们的生存期比典型 ASP.NET Core 应用中的 Scoped 服务要长。

限制 Blazor 应用中服务生存期的一种方法是使用 OwningComponentBase 类型。OwningComponentBase 是派生自 ComponentBase 的一种抽象类型,它会创建与组件生存期相对应的 DI 范围。通过使用此范围,可使用具有 Scoped 生存期的 DI 服务,并使其生存期与组件的生存期一样长。销毁组件时,也会处置组件的 Scoped 服务提供程序提供的服务。这对以下服务很有用:

  • 由于 Transient 生存期不适用而应在组件中重复使用的服务。
  • 由于 Singleton 生存期不适用而不得跨组件共享的服务。

可使用下面两个版本的 OwningComponentBase 类型:

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

使用 @injectInjectAttribute ([Inject]) 注入到组件中的 DI 服务不是在组件的范围中创建的。要使用组件的范围,必须使用 ScopedServices.GetRequiredServiceScopedServices.GetService 解析服务。任何使用 ScopedServices 提供程序进行解析的服务都具有从同一范围提供的依赖关系。

  1. @page "/preferences"
  2. @using Microsoft.Extensions.DependencyInjection
  3. @inherits OwningComponentBase
  4. <h1>User (@UserService.Name)</h1>
  5. <ul>
  6. @foreach (var setting in SettingService.GetSettings())
  7. {
  8. <li>@setting.SettingName: @setting.SettingValue</li>
  9. }
  10. </ul>
  11. @code {
  12. private IUserService UserService { get; set; }
  13. private ISettingService SettingService { get; set; }
  14. protected override void OnInitialized()
  15. {
  16. UserService = ScopedServices.GetRequiredService<IUserService>();
  17. SettingService = ScopedServices.GetRequiredService<ISettingService>();
  18. }
  19. }
  • OwningComponentBase<T> 派生自 OwningComponentBase,并添加了一个 Service 属性,该属性从 Scoped DI 提供程序返回 T 的实例。当存在一项应用需要从使用组件范围的 DI 容器中获取的主服务时,不必使用 IServiceProvider 的实例即可通过此类型便捷地访问 Scoped 服务。ScopedServices 属性可用,因此应用可获取其他类型的服务(如有必要)。
  1. @page "/users"
  2. @attribute [Authorize]
  3. @inherits OwningComponentBase<AppDbContext>
  4. <h1>Users (@Service.Users.Count())</h1>
  5. <ul>
  6. @foreach (var user in Service.Users)
  7. {
  8. <li>@user.UserName</li>
  9. }
  10. </ul>

使用来自 DI 的实体框架 DbContextUse of Entity Framework DbContext from DI

从 Web 应用中的 DI 检索的一种常见服务类型是实体框架 (EF) DbContext 对象。默认情况下,使用 IServiceCollection.AddDbContext 注册 EF 服务会将 DbContext 添加为一项 Scoped 服务。注册为 Scoped 服务可能会导致 Blazor 应用中出现问题,因为这会导致 DbContext 实例生存期较长且跨应用共享。DbContext 不是线程安全的且不得同时使用。

根据应用的不同,使用 OwningComponentBaseDbContext 的范围限制为单个组件可能会解决此问题 。如果组件不并行使用 DbContext,则从 OwningComponentBase 派生该组件并从 ScopedServices 检索 DbContext 就已足够,因为它可确保:

  • 单独的组件不共享 DbContext
  • DbContext 的生存期与依赖它的组件的生存期一样长。

如果单个组件可能同时使用 DbContext(例如用户每次选择一个按钮),则即使使用 OwningComponentBase 也不能避免并发 EF 操作问题。在这种情况下,请对每个逻辑 EF 操作使用不同的 DbContext请使用下述任一方法:

  • 使用 DbContextOptions<TContext> 作为参数直接创建 DbContext,这可从 DI 进行检索且是线程安全的。
  1. @page "/example"
  2. @inject DbContextOptions<AppDbContext> DbContextOptions
  3. <ul>
  4. @foreach (var item in _data)
  5. {
  6. <li>@item</li>
  7. }
  8. </ul>
  9. <button @onclick="LoadData">Load Data</button>
  10. @code {
  11. private List<string> _data = new List<string>();
  12. private async Task LoadData()
  13. {
  14. _data = await GetAsync();
  15. StateHasChanged();
  16. }
  17. public async Task<List<string>> GetAsync()
  18. {
  19. using (var context = new AppDbContext(DbContextOptions))
  20. {
  21. return await context.Products.Select(p => p.Name).ToListAsync();
  22. }
  23. }
  24. }
  • 在具有 Transient 生存期的服务容器中注册 DbContext

    • 注册上下文时,请使用 ServiceLifetime.TransientAddDbContext 扩展方法采用两个 ServiceLifetime 类型的可选参数。若要使用此方法,则只有 contextLifetime 参数需要设为 ServiceLifetime.TransientoptionsLifetime 可保留其默认值 ServiceLifetime.Scoped
  1. services.AddDbContext<AppDbContext>(options =>
  2. options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")),
  3. ServiceLifetime.Transient);
  • 可将暂时性 DbContext 正常注入(使用 @inject)到不会并行执行多个 EF 操作的组件。可能同时执行多个 EF 操作的人员可使用 IServiceProvider.GetRequiredService 为每个并行操作请求单独的 DbContext 对象。
  1. @page "/example"
  2. @using Microsoft.Extensions.DependencyInjection
  3. @inject IServiceProvider ServiceProvider
  4. <ul>
  5. @foreach (var item in _data)
  6. {
  7. <li>@item</li>
  8. }
  9. </ul>
  10. <button @onclick="LoadData">Load Data</button>
  11. @code {
  12. private List<string> _data = new List<string>();
  13. private async Task LoadData()
  14. {
  15. _data = await GetAsync();
  16. StateHasChanged();
  17. }
  18. public async Task<List<string>> GetAsync()
  19. {
  20. using (var context = ServiceProvider.GetRequiredService<AppDbContext>())
  21. {
  22. return await context.Products.Select(p => p.Name).ToListAsync();
  23. }
  24. }
  25. }

其他资源Additional resources