.Net8 新特性之依赖注入容器对Keyed Service的支持

本译自:Keyed service dependency injection container support

在这篇文章中,我将讨论 .NET 8 预览版 7 中引入的对依赖关系注入容器的新“Keyed Service”支持。我将介绍如何使用Keyed Service、何时使用它们,以及它们在后台的工作方式。

由于这些帖子都使用预览版,因此在 .NET 8 最终于 2023 年 11 月发布之前,某些功能可能会更改(或删除)!

什么是Keyed service?

依赖注入 (DI) 在 ASP.NET Core 中无处不在。您可以将其与自定义服务一起使用,但也许更重要的是,框架本身始终使用 DI。您可以在 ASP.NET Core 中配置的大多数内容都是通过 DI 配置的。
因此,ASP.NET Core 附带了一个内置的 DI 容器(也可作为 Microsoft.Extensions.DependencyInjection 使用)。这个容器在很多方面都非常基本。它是一个符合要求的容器,用于定义 DI 容器必须具有的最低功能。您可以随时添加第三方容器,例如 Lamar 或 Autofac,但许多人坚持使用内置容器,因为它是默认的。

提醒一下,我的新书《ASP.NET Core in Action, Third Edition》详细介绍了依赖注入,包括如何使用第三方容器。更重要的是,您目前只能在 8 月 17 日之前使用代码 pblock3 在 manning.com 获得 45% 的折扣,所以如果您正在考虑购买它,现在是时候抓住它了!

向内置容器注册服务时,只能控制三件事:

  • Lifetime - 用于控制服务实例的重用频率,可以是以下三个值之一: Transient 、 Scoped 或 Singleton 。
  • ServiceType — 这是您在构造函数中“请求”的类型。它可以是接口,如 ,也可以是具体类型,如 IWidget Widget 。
  • ImplementationType instance - 这是用于满足依赖关系的 ServiceType 类型(或实例),例如 Widget 。
    围绕这些注册有很多帮助程序和重载,但从根本上说,服务注册仅包含这些信息,存储在一起 ServiceDescriptor 作为...

戏剧性的停顿......切换到戏剧性的电影配音......

直到现在。
对于Keyed Service,另一条信息与标识服务的 ServiceDescriptor 一起 ServiceKey 存储。键可以是任何对象,但它通常是 a string 或 an enum (可以是常量,因此可以在属性中使用)。对于非密钥服务,标识 ServiceType 注册;对于键控服务,标识注册的 ServiceType 和 ServiceKey 的组合。

此功能是 .NET 8 中内置 DI 容器的新增功能,但它在其他 DI 容器中已提供很长时间。例如,Structuremap 具有与 Autofac 相同的功能,称为“命名服务”。

这是简单的解释,所以现在让我们看一个如何使用键控服务的基本示例。

使用键控服务检索特定服务实例

在展示示例之前,我将指出当前一个明显的局限性:最小 API 和 MVC 目前都不直接支持键控服务。这里这里记录了这些问题,但缺乏支持使以下示例比我想要的😅更复杂
当您的接口/服务具有要在应用中使用的多个实现时,键控服务非常有用。此外,您需要在应用的不同位置使用这些实现。
例如,请考虑以下接口:

public interface INotificationService
{
    string Notify(string message);
}

我们有三个简单的占位符实现:

public class SmsNotificationService : INotificationService
{
   public string Notify(string message) => $"[SMS] {message}";
}

public class EmailNotificationService : INotificationService
{
   public string Notify(string message) => $"[Email] {message}";
}

public class PushNotificationService : INotificationService
{
   public string Notify(string message) => $"[Push] {message}";
}

没有Keyed Service

如果没有键控服务,您可以像这样注册所有这些服务:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<INotificationService, SmsNotificationService>();
builder.Services.AddSingleton<INotificationService, EmailNotificationService>();
builder.Services.AddSingleton<INotificationService, PushNotificationService>();

但是,您只能像这样检索所有服务:

public class NotifierService(IEnumerable<INotificationService> services){}

或者,您可以像这样只检索上次注册的服务 ( PushNotificationService )

public class NotifierService(INotificationService service){}

以前,没有一种简单的方法可以直接检索 SmsNotificationService or EmailNotificationService ,同时仍将它们注册为 INotificationService .有了键控服务,这成为可能。

请注意,一直有解决方法,例如将服务注册为具体类型,以及委派 INotificationService 注册。我在上一篇文章中描述了这种方法,但它总是感觉有点像黑客。

使用Keyed Service

若要注册键控服务,请使用 AddKeyedSingleton() 、 AddKeyedScoped() 或 AddKeyedTransient() 重载之一,并提供对象作为键。在以下示例中,我使用了 string ,但您可能希望使用 enum or 共享常量,例如:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedSingleton<INotificationService, PushNotificationService>("push");

若要检索键控服务,请将该 [FromKeyedServices(object key)] 属性应用于服务构造函数中的参数。以下示例使用 C#12 功能(主构造函数)演示了这一点:

// Uses the key "sms" to select the SmsNotificationService specifically
public class SmsWrapper([FromKeyedServices("sms")] INotificationService sms)
{
    public string Notify(string message) => sms.Notify(message);
}

// Uses the key "email" to select the EmailNotificationService specifically
public class EmailWrapper([FromKeyedServices("email")] INotificationService email)
{
    public string Notify(string message) => email.Notify(message);
}

然后,我们需要注册包装器服务,然后可以在最小的 API 或 MVC 中使用这些服务。注册和使用将如下所示:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedSingleton<INotificationService, PushNotificationService>("push");
builder.Services.AddSingleton<SmsWrapper>();
builder.Services.AddSingleton<EmailWrapper>();
var app = builder.Build();

app.MapGet("/sms", (SmsWrapper notifier) => notifier.Sms("Hello world"));
app.MapGet("/email", (EmailWrapper notifier) => notifier.Email("Hello world"));

app.Run();

这显然是一个人为的例子,但在很多情况下,你可能想做类似的事情。正如我之前提到的,使用工厂模式或“双重注册”总是有办法解决这个问题,但有些人可能更喜欢键控服务的直接 DI 方法。
然而,当前的Keyed Service实现存在一些局限性。

Keyed Service

我已经提到过,我遇到的第一个限制是,您目前无法 [FromKeyedServices] 直接在最小的 API 中使用,如下所示:

                    // ⚠ don't do this, you'll get runtime errors
app.MapGet("/sms", ([FromKeyedServices("sms")] INotificationService service) 
                         => service.Notify("Hello world"));

遗憾的是,最小 API 无法识别该 [FromKeyedServices] 属性,而是会尝试将服务绑定到请求正文。如果在 GET 请求中使用它,如上面的示例所示,则在运行时将出现异常,如下所示:

image

另一个有趣的点是,如果启用最小 API 源生成器,它似乎无法正确🤷 ♂️拦截该方法
好消息是,对这种模式的支持已经在跟踪中,并计划包含在 .NET 8 RC1 中。它是否会做到这一点,我想我们会看到,但它几乎肯定会在最终的 .NET 8 版本中。
一个类似的问题是 MVC 中的跟踪 [FromKeyedServices] 支持,MVC 也计划包含在 RC1 中。同时,如果你迫切希望使用键控服务,则需要使用我在上一节中展示的“包装器”方法。
我遇到的另一个限制是 .NET 8 预览版 7 公告帖子中演示的 IKeyedServiceProvider 。长话短说;演示的代码在与作用域内服务提供程序一起使用时不起作用:

// ⚠ Shows injecting an IKeyedServiceProvider, but this doesn't work in preview 7
class SmallCacheConsumer(IKeyedServiceProvider keyedServiceProvider)
{
     public object? GetData() 
          => keyedServiceProvider.GetRequiredKeyedService<IMemoryCache>("small");
}

问题在于,在 .NET 8 预览版 7 版本中, IKeyedServiceProvider 未向 DI 容器注册。此外,从 DI 容器请求 IServiceProvider 时,通常会获得一个作用域内的服务提供商, ServiceProviderEngineScope 该提供程序在 IServiceProvider 预览版 7 版本中实现但未实现 IKeyedServiceProvider 。这意味着你也不能做这样的事情:

// ⚠ This also doesn't work in preview 7
class SmallCacheConsumer(IServiceProvider serviceProvider)
{
     public object? GetData() 
          => serviceProvider.GetRequiredKeyedService<IMemoryCache>("small");
}

ServiceProviderEngineScope 确实在当前的夜间代码中实现 IKeyedServiceProvider ),所以我毫不怀疑这将在下一个预览版/RC 版本中得到解决,但与此同时,这是需要注意的另一件事。

有趣的是,该 ServiceProvider 类型已经实现了 IKeyedServiceProvider ,这意味着您已经可以直接从根 IServiceProvider 解析键控服务。例如:

var builder = WebApplication.CreateBuilder(args);
// Add keyed services
builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");
var app = builder.Build();

// 👇 Resolving from the root container DOES work in preview 7
var smsService = app.Services.GetRequiredKeyedService<INotificationService>("sms");

希望这些问题都应该在最终的 .NET 8 版本中得到解决,但如果你使用的是预览版,请记住这些问题。

探索边缘案例

在典型的 ASP.NET Core 应用程序中,大部分 DI 交互是通过服务构造函数的组合进行的,因此您可能会使用 作为 [FromKeyedServices] 检索Keyed Service的主要方式,正如您在本文前面看到的那样。然而,这一切的运作方式有多种细微差别。在本节中,我将演示其中的一些微妙之处。
考虑所有这些的最简单方法是记住,Keyed Service的工作方式与非Keyed Service相同。唯一的区别是如何识别容器中的服务:

  • Keyed Service使用ServiceType和ServiceKey 来定义其唯一性
  • 非Keyed Service使用 a ServiceType 来定义其唯一性。或者,您可以将其视为与键控服务相同, ServiceKey null
    一旦您意识到非密钥服务和密钥服务的规则本质上是相同的,那么以下边缘情况都是有道理的!
    使用同一Key注册多个服务
    内置的 DI 容器允许您使用多个实现注册同一服务:
builder.Services.AddSingleton<INotificationService, SmsNotificationService>();
builder.Services.AddSingleton<INotificationService, EmailNotificationService>();

同样,您可以多次使用相同的密钥注册密钥服务,例如

builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("sms");

这是完全有效的,所以一定要注意这种事情中的错误!
检索使用同一Key注册的多个服务
如果有意使用同一Key注册多个服务,则可能需要检索这些服务。而且,不出所料,您可以在与非Keyed Service相同的位置检索所有已注册的Keyed Service。
例如,要检索构造函数中的所有实例,请注入 IEnumerable 并装饰 [FromKeyedServices] :

//  Add the keyed services attribute with the key 👇     👇 and inject as IEnumerable<>
public class NotifierService([FromKeyedServices("sms")] IEnumerable<INotificationService> smsServices)
{
}

如果注入 INotificationService 的单个实例,则就像非键控服务一样,“最后注册的服务”获胜:

//  Add the keyed services attribute with the key 👇
public class NotifierService([FromKeyedServices("sms")] INotificationService smsService)
{
     // smsService is EmailNotificationService
}

有条件地注册和删除Keyed Service
已经有许多 IServiceCollection的扩展方法用于注册服务,现在还有一大堆用于有条件地注册Keyed Service (TryAdd*) 和删除服务 ( RemoveAllKeyed)。这些都是不言自明的,而且数量众多,主要是因为 每个生命周期都有重复

namespace Microsoft.Extensions.DependencyInjection.Extensions {
     public static class ServiceCollectionDescriptorExtensions {
+        public static IServiceCollection RemoveAllKeyed(this IServiceCollection collection, Type serviceType, object? serviceKey);
+        public static IServiceCollection RemoveAllKeyed<T>(this IServiceCollection collection, object? serviceKey);
+        public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object? serviceKey);
+        public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object? serviceKey, Func<IServiceProvider, object, object> implementationFactory);
+        public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object? serviceKey, Type implementationType);
+        public static void TryAddKeyedScoped<TService, TImplementation>(this IServiceCollection collection, object? serviceKey) where TService : class where TImplementation : class, TService;
+        public static void TryAddKeyedScoped<TService>(this IServiceCollection collection, object? serviceKey) where TService : class;
+        public static void TryAddKeyedScoped<TService>(this IServiceCollection services, object? serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
+        public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object? serviceKey);
+        public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object? serviceKey, Func<IServiceProvider, object, object> implementationFactory);
+        public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object? serviceKey, Type implementationType);
+        public static void TryAddKeyedSingleton<TService, TImplementation>(this IServiceCollection collection, object? serviceKey) where TService : class where TImplementation : class, TService;
+        public static void TryAddKeyedSingleton<TService>(this IServiceCollection collection, object? serviceKey) where TService : class;
+        public static void TryAddKeyedSingleton<TService>(this IServiceCollection services, object? serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
+        public static void TryAddKeyedSingleton<TService>(this IServiceCollection collection, object? serviceKey, TService instance) where TService : class;
+        public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object? serviceKey);
+        public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object? serviceKey, Func<IServiceProvider, object, object> implementationFactory);
+        public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object? serviceKey, Type implementationType);
+        public static void TryAddKeyedTransient<TService, TImplementation>(this IServiceCollection collection, object? serviceKey) where TService : class where TImplementation : class, TService;
+        public static void TryAddKeyedTransient<TService>(this IServiceCollection collection, object? serviceKey) where TService : class;
+        public static void TryAddKeyedTransient<TService>(this IServiceCollection services, object? serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
     }
 }

这就是它的全部内容。Keyed Service简化了一些服务组合要求,因此,如果您当前正在使用 DI“变通办法”之一,您可能会发现它们很有用。但是Keyed Service并不是绝对必要的,所以不要觉得他们存在就必须使用它们!🙂

总结

在这篇文章中,我介绍了添加到 .NET 8 预览版 7 中发布的内置依赖项注入容器中的新Keyed Service功能。您可以使用以下命令 AddKeyedSingleton(object? key) 将服务添加为“Keyed”服务(类似地,对于瞬态或作用域内服务)。若要将Keyed Service注入构造函数,请使用该 [FromKeyedServices(object key)] 属性修饰参数,并传入用于注册服务的相同键。还可以使用 IKeyedServiceProvider “即服务”定位器来检索键控服务。

当前实现存在一些限制:目前无法在最小 API 或 MVC/Razor Pages 中使用 [FromKeyedServices] ,并且还不能注入到服务构造函数 IKeyedServiceProvider 中。这些限制都应该在 11 月发布 .NET 8 时得到解决。

posted @ 2023-11-04 07:15  dongfo  阅读(521)  评论(0编辑  收藏  举报