.NET 8新特性之KeyedService

发布时间 2023-11-16 12:05:08作者: 农民小工程师
简介
.NET 8 在 Preview 7 中引入了 KeyedService 支持,以后我们可以方便支持按 name 来获取 service 了,有些情况下就不用自己创建一个 factory 了。

例子
GetStarted
来看使用一个基本的使用示例:

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.AddKeyedSingleton<IUserIdProvider, EnvironmentUserIdProvider>("env");
  3. serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>("");
  4. // 堆代码 duidaima.com
  5. using var services = serviceCollection.BuildServiceProvider();
  6. var userIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("");
  7. Console.WriteLine(userIdProvider.GetUserId());
  8. var envUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("env");
  9. Console.WriteLine(envUserIdProvider.GetUserId());
  10. file interface IUserIdProvider
  11. {
  12. string GetUserId();
  13. }
  14. file sealed class EnvUserIdProvider: IUserIdProvider
  15. {
  16. public string GetUserId() => Environment.MachineName;
  17. }
  18. file sealed class NullUserIdProvider: IUserIdProvider
  19. {
  20. public string GetUserId() => "(null)";
  21. }
输出结果如下:

codeduidaima.com

  1. (null)
  2. WEIHANLI-SURFACE

AnyKey

serviceKey 有一个特殊的存在 KeyedService.AnyKey 我们可以用这个来捕获未注册的 serviceKey,示例如下:

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(KeyedService.AnyKey);
  3. using var services = serviceCollection.BuildServiceProvider();
  4. var userIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("");
  5. Console.WriteLine(userIdProvider.GetUserId());
  6. var envUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("env");
  7. Console.WriteLine(envUserIdProvider.GetUserId());
可以看到我们注册服务的时候使用的是 KeyedService.AnyKey, 获取服务的时候并没有使用这个 key 使用的是未经注册的 serviceKey 。
输出结果如下:

codeduidaima.com

  1. (null)
  2. (null)
可以看到这两个 serviceKey 拿到的 service 并没有报错,使用了 AnyKey 注册的服务。那他们两个会是同一个对象吗还是两个对象呢,我们可以很简单地进行一下验证

codeduidaima.com

  1. Console.WriteLine("userIdProvider == envUserIdProvider ?? {0}", userIdProvider == envUserIdProvider);
输出结果如下:

codeduidaima.com

  1. userIdProvider == envUserIdProvider ?? False
由此可以看到实际每个 serviceKey 是一个对象,不同的 serviceKey  是不同的对象。serviceKey 还有一个特殊情况,目前的 API 里 KeyedService 相关的 API 里 serviceKey 是允许为 null 的,但是实际上当 serviceKey 为 null 时它就不是一个 keyed service 了,我个人觉得这个 API 的设计是有些问题的,不应该允许 null,来看一个示例:

codeduidaima.com

  1. var nullUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(null);
  2. Console.WriteLine(nullUserIdProvider.GetUserId());
输出结果如下:

codeduidaima.com

  1. System.InvalidOperationException: No service for type 'Net8Sample.<__Script>FE1DBF3BE6F8384813B223E3EAA03DBABDC4153F95C5B3EBB0E0807E84E7C20E4__IUserIdProvider' has been registered.
  2. at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetRequiredKeyedService(Type serviceType, Object serviceKey)
  3. at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey)
可以看到当 serviceKey 为 null 时,实际并不会像之前一样使用 AnyKey 对应的服务,会直接报错,如果使用 keyedService 则不应该使用 null 作为 serviceKey 。

另外如果我们注册 keyed service 的时候使用 null 作为 serviceKey,实际相当于注册了一个非 keyed service,比如说这两种注册方式是等价的

codeduidaima.com

  1. serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(null);
  2. serviceCollection.AddSingleton<IUserIdProvider, NullUserIdProvider>();
我们在获取服务的时候都可以使用 GetRequiredService<IUserIdProvider>() 来获取服务示例,目前使用 GetRequiredKeyedService<IUserIdProvider>(null) 也是可以的

ServiceKey in constructor
在构造方法中可以使用 ServiceKeyAttribute 来在构造方法中获取注册的 serviceKey,我们来看一个示例:

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.AddKeyedTransient<MyNamedService>(KeyedService.AnyKey);
  3. using var services = serviceCollection.BuildServiceProvider();
  4. Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>("Foo").Name);
  5. Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>("Hello").Name);
  6. file sealed class MyNamedService
  7. {
  8. public MyNamedService([ServiceKey]string name)
  9. {
  10. Name = name;
  11. }
  12. public string Name { get; }
  13. }
我们使用 KeyedService.AnyKey 来注册服务,在构造方法里获取 serviceKey 输出结果如下:

codeduidaima.com

  1. Foo
  2. Hello
可以看到我们输出的结果正确反映了我们实际期望的 serviceKey

这里需要注意的是我们需要保证 constructor 中的 serviceKey 类型和获取服务时的类型应该是一致的,否则会有异常,比如:

codeduidaima.com

  1. Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>(123).Name);
这样会导致下面的异常:

codeduidaima.com

  1. System.InvalidOperationException: The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute.
  2. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
  3. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain)
  4. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot)
  5. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
  6. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
  7. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain)
  8. at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
  9. at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
  10. at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
  11. at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetKeyedService(Type serviceType, Object serviceKey)
  12. at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetRequiredKeyedService(Type serviceType, Object serviceKey)
  13. at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey)
serviceKey 是 object 类型,所以我们是可以用任意公平法务类型的,比如说下面这个示例:

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.AddKeyedTransient<MyKeyedService>(KeyedService.AnyKey);
  3. using var services = serviceCollection.BuildServiceProvider();
  4. Console.WriteLine(services.GetRequiredKeyedService<MyKeyedService>(new Category()
  5. {
  6. Id = 1,
  7. Name = "test"
  8. }).Name);
将会输出 test

Scoped Sevice
目前对于 scoped service 的支持是有些问题的,使用 scoped service 使用会发生异常

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.AddKeyedScoped<IUserIdProvider, NullUserIdProvider>("");
  3. using var services = serviceCollection.BuildServiceProvider();
  4. using var scope = services.CreateScope();
  5. var newId = scope.ServiceProvider.GetRequiredKeyedService<IIdGenerator>("").NewId();
  6. Console.WriteLine(newId);
会看到下面这样的一个异常:

codeduidaima.com

  1. System.InvalidOperationException: This service provider doesn't support keyed services.
  2. at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService(IServiceProvider provider, Type serviceType, Object serviceKey)
  3. at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey)
  4. at Net8Sample.KeyedServiceSample.ScopedSample()
基于此,如果在 aspnetcore 里基于 HttpContext.RequestServices 去获取 keyedService 的话都会有这样的一个异常,因为 HttpContext.RequestServices 也是一个 scoped service provider

感兴趣的可以尝试一下下面的示例,看看两个 API 的 response:

codeduidaima.com

  1. var builder = WebApplication.CreateBuilder();
  2. builder.Services.AddKeyedSingleton<IIdGenerator, GuidIdGenerator>("guid");
  3. var app = builder.Build();
  4. app.Map("/id0", ([FromKeyedServices("guid")]IIdGenerator idGenerator)
  5. => Result.Success<string>(idGenerator.NewId()));
  6. app.Map("/id", (HttpContext httpContext) =>
  7. {
  8. var idGenerator = httpContext.RequestServices.GetRequiredKeyedService<IIdGenerator>("guid");
  9. return Result.Success<string>(idGenerator.NewId());
  10. });
  11. await app.RunAsync();
主要原因是 ScopedServiceProvider 没有实现 IKeyedServiceProvider, 已经有 PR 修复了这个问题,在 RC1 版本中应该会发布,应该会够修复这个问题

其它
我们也可以结合 Options 来方便的实现基于 options 的 named service,示例如下:

codeduidaima.com

  1. var serviceCollection = new ServiceCollection();
  2. serviceCollection.Configure<TotpOptions>(x =>
  3. {
  4. x.Salt = "1234";
  5. });
  6. serviceCollection.AddKeyedTransient<ITotpService, TotpService>(KeyedService.AnyKey,
  7. (sp, key)=>
  8. new TotpService(sp.GetRequiredService<IOptionsMonitor<TotpOptions>>()
  9. .Get(key is string name ? name : Options.DefaultName)));
  10. using var services = serviceCollection.BuildServiceProvider(https://www.523it.com/);
  11. var totpService = services.GetRequiredKeyedService<ITotpService>(string.Empty);
  12. Console.WriteLine("Totp1: {0}", totpService.GetCode("Test1234"));
  13. var totpService2 = services.GetRequiredKeyedService<ITotpService>("test");
  14. Console.WriteLine("Totp2: {0}", totpService2.GetCode("Test1234"));
输出结果如下:

codeduidaima.com

  1. Totp1: 356934
  2. Totp2: 626994
总体上来说,感觉解决了一些 named service 的一些痛点,可惜的是还有一些 bug,不过目前是预览版还能接受,正式版只要能够正常使用就可以。另外觉得 serviceKey 可以为 null 觉得有些不合理,既然是 keyedService 那应该就不允许为 null 如果为 null 了就不是 keyedSevice 了。