.NET 8 中的 KeyedService
The following article is from amazingdotnet Author WeihanLi
Intro
.NET 8 在 Preview 7 中引入了 KeyedService 支持,以后我们可以方便支持按 name 来获取 service 了,有些情况下就不用自己创建一个 factory 了。
Sample
GetStarted
来看使用一个基本的使用示例:
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedSingleton<IUserIdProvider, EnvironmentUserIdProvider>("env");
serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>("");
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
另外如果我们注册 keyed service 的时候使用 null
作为 serviceKey
,实际相当于注册了一个非 keyed service,比如说这两种注册方式是等价的
serviceCollection.AddKeyedSingleton<IUserIdProvider, NullUserIdProvider>(null);
serviceCollection.AddSingleton<IUserIdProvider, NullUserIdProvider>();
我们在获取服务的时候都可以使用 GetRequiredService<IUserIdProvider>()
来获取服务示例,目前使用 GetRequiredKeyedService<IUserIdProvider>(null)
也是可以的
ServiceKey in constructor
在构造方法中可以使用 ServiceKeyAttribute
来在构造方法中获取注册的 serviceKey
,我们来看一个示例:
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这里需要注意的是我们需要保证 constructor 中的 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
Scoped Sevice
目前对于 scoped service 的支持是有些问题的,使用 scoped service 使用会发生异常
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感兴趣的可以尝试一下下面的示例,看看两个 API 的 response:
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 版本中应该会发布,应该会够修复这个问题
More
我们也可以结合 Options 来方便的实现基于 options 的 named service,示例如下:
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 了前面示例代码都在 Github 上,有需要的小伙伴可以自取:https://github.com/WeihanLi/SamplesInPractice/blob/master/net8sample/Net8Sample/KeyedServiceSample.cs
- EOF -
看完本文有收获?请转发分享给更多人
推荐关注「DotNet」,提升.Net技能
点赞和在看就是最大的支持❤️