查看原文
其他

.NET 8 中的 KeyedService

DotNet 2024-04-12

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 EnvUserIdProviderIUserIdProvider
{
    public string GetUserId() => Environment.MachineName;
}
file sealed class NullUserIdProviderIUserIdProvider
{
    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 的,但是实际上当 serviceKeynull 时它就不是一个 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)

可以看到当 serviceKeynull 时,实际并不会像之前一样使用 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 -

推荐阅读  点击标题可跳转

五分钟看完,彻底理解协变逆变

C# 获取文件信息大全

C# 中关于 T 泛型

看完本文有收获?请转发分享给更多人

推荐关注「DotNet」,提升.Net技能 

点赞和在看就是最大的支持❤️

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存