var serviceCollection = new ServiceCollection(); serviceCollection.AddKeyedSingleton<IUserIdProvider, EnvironmentUserIdProvider>("env"); serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(""); // 堆代码 duidaima.com using var services = serviceCollection.BuildServiceProvider(); var userIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(""); Console.WriteLine(userIdProvider.GetUserId()); var envUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("env"); Console.WriteLine(envUserIdProvider.GetUserId()); file interface IUserIdProvider { string GetUserId(); } file sealed class EnvUserIdProvider: IUserIdProvider { public string GetUserId() => Environment.MachineName; } file sealed class NullUserIdProvider: IUserIdProvider { public string GetUserId() => "(null)"; }输出结果如下:
(null) WEIHANLI-SURFACE
AnyKey
serviceKey 有一个特殊的存在 KeyedService.AnyKey 我们可以用这个来捕获未注册的 serviceKey,示例如下:
var serviceCollection = new ServiceCollection(); serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(KeyedService.AnyKey); using var services = serviceCollection.BuildServiceProvider(); var userIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(""); Console.WriteLine(userIdProvider.GetUserId()); var envUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>("env"); Console.WriteLine(envUserIdProvider.GetUserId());可以看到我们注册服务的时候使用的是 KeyedService.AnyKey, 获取服务的时候并没有使用这个 key 使用的是未经注册的 serviceKey 。
(null) (null)可以看到这两个 serviceKey 拿到的 service 并没有报错,使用了 AnyKey 注册的服务。那他们两个会是同一个对象吗还是两个对象呢,我们可以很简单地进行一下验证
Console.WriteLine("userIdProvider == envUserIdProvider ?? {0}", userIdProvider == envUserIdProvider);输出结果如下:
userIdProvider == envUserIdProvider ?? False由此可以看到实际每个 serviceKey 是一个对象,不同的 serviceKey 是不同的对象。serviceKey 还有一个特殊情况,目前的 API 里 KeyedService 相关的 API 里 serviceKey 是允许为 null 的,但是实际上当 serviceKey 为 null 时它就不是一个 keyed service 了,我个人觉得这个 API 的设计是有些问题的,不应该允许 null,来看一个示例:
var nullUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(null); Console.WriteLine(nullUserIdProvider.GetUserId());输出结果如下:
System.InvalidOperationException: No service for type 'Net8Sample.<__Script>FE1DBF3BE6F8384813B223E3EAA03DBABDC4153F95C5B3EBB0E0807E84E7C20E4__IUserIdProvider' has been registered. at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetRequiredKeyedService(Type serviceType, Object serviceKey) at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey)可以看到当 serviceKey 为 null 时,实际并不会像之前一样使用 AnyKey 对应的服务,会直接报错,如果使用 keyedService 则不应该使用 null 作为 serviceKey 。
serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(null); serviceCollection.AddSingleton<IUserIdProvider, NullUserIdProvider>();我们在获取服务的时候都可以使用 GetRequiredService<IUserIdProvider>() 来获取服务示例,目前使用 GetRequiredKeyedService<IUserIdProvider>(null) 也是可以的
var serviceCollection = new ServiceCollection(); serviceCollection.AddKeyedTransient<MyNamedService>(KeyedService.AnyKey); using var services = serviceCollection.BuildServiceProvider(); Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>("Foo").Name); Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>("Hello").Name); file sealed class MyNamedService { public MyNamedService([ServiceKey]string name) { Name = name; } public string Name { get; } }我们使用 KeyedService.AnyKey 来注册服务,在构造方法里获取 serviceKey 输出结果如下:
Foo Hello可以看到我们输出的结果正确反映了我们实际期望的 serviceKey
Console.WriteLine(services.GetRequiredKeyedService<MyNamedService>(123).Name);这样会导致下面的异常:
System.InvalidOperationException: The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain, Int32 slot) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier) at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetKeyedService(Type serviceType, Object serviceKey) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetRequiredKeyedService(Type serviceType, Object serviceKey) at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey)serviceKey 是 object 类型,所以我们是可以用任意类型的,比如说下面这个示例:
var serviceCollection = new ServiceCollection(); serviceCollection.AddKeyedTransient<MyKeyedService>(KeyedService.AnyKey); using var services = serviceCollection.BuildServiceProvider(); Console.WriteLine(services.GetRequiredKeyedService<MyKeyedService>(new Category() { Id = 1, Name = "test" }).Name);将会输出 test
var serviceCollection = new ServiceCollection(); serviceCollection.AddKeyedScoped<IUserIdProvider, NullUserIdProvider>(""); using var services = serviceCollection.BuildServiceProvider(); using var scope = services.CreateScope(); var newId = scope.ServiceProvider.GetRequiredKeyedService<IIdGenerator>("").NewId(); Console.WriteLine(newId);会看到下面这样的一个异常:
System.InvalidOperationException: This service provider doesn't support keyed services. at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService(IServiceProvider provider, Type serviceType, Object serviceKey) at Microsoft.Extensions.DependencyInjection.ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService[T](IServiceProvider provider, Object serviceKey) at Net8Sample.KeyedServiceSample.ScopedSample()基于此,如果在 aspnetcore 里基于 HttpContext.RequestServices 去获取 keyedService 的话都会有这样的一个异常,因为 HttpContext.RequestServices 也是一个 scoped service provider
var builder = WebApplication.CreateBuilder(); builder.Services.AddKeyedSingleton<IIdGenerator, GuidIdGenerator>("guid"); var app = builder.Build(); app.Map("/id0", ([FromKeyedServices("guid")]IIdGenerator idGenerator) => Result.Success<string>(idGenerator.NewId())); app.Map("/id", (HttpContext httpContext) => { var idGenerator = httpContext.RequestServices.GetRequiredKeyedService<IIdGenerator>("guid"); return Result.Success<string>(idGenerator.NewId()); }); await app.RunAsync();主要原因是 ScopedServiceProvider 没有实现 IKeyedServiceProvider, 已经有 PR 修复了这个问题,在 RC1 版本中应该会发布,应该会够修复这个问题
var serviceCollection = new ServiceCollection(); serviceCollection.Configure<TotpOptions>(x => { x.Salt = "1234"; }); serviceCollection.AddKeyedTransient<ITotpService, TotpService>(KeyedService.AnyKey, (sp, key)=> new TotpService(sp.GetRequiredService<IOptionsMonitor<TotpOptions>>() .Get(key is string name ? name : Options.DefaultName))); using var services = serviceCollection.BuildServiceProvider(); var totpService = services.GetRequiredKeyedService<ITotpService>(string.Empty); Console.WriteLine("Totp1: {0}", totpService.GetCode("Test1234")); var totpService2 = services.GetRequiredKeyedService<ITotpService>("test"); Console.WriteLine("Totp2: {0}", totpService2.GetCode("Test1234"));输出结果如下:
Totp1: 356934 Totp2: 626994总体上来说,感觉解决了一些 named service 的一些痛点,可惜的是还有一些 bug,不过目前是预览版还能接受,正式版只要能够正常使用就可以。另外觉得 serviceKey 可以为 null 觉得有些不合理,既然是 keyedService 那应该就不允许为 null 如果为 null 了就不是 keyedSevice 了。