在 .NET 8 的 Preview 7 版本中,引入了 KeyedService 支持。这一特性为开发者提供了按名称(name)获取服务的便利,在某些场景下,开发者无需再自行创建工厂类来管理服务。接下来,我们将深入探讨 KeyedService 的使用方法、特殊情况以及存在的一些问题。
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)";
}
上述代码展示了 KeyedService 的基本使用。我们通过 AddKeyedSingleton
方法注册了两个不同的 IUserIdProvider
实现,并分别使用不同的键(“env” 和 “”)进行标识。然后,通过 GetRequiredKeyedService
方法根据键来获取相应的服务实例。
运行代码后,输出结果为:
(null)
WEIHANLI - SURFACE
这表明我们成功地根据不同的键获取到了对应的服务实例,并调用了其方法。
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
来注册服务。当我们获取服务时,即使使用了未注册的键(如 “” 和 “env”),也不会报错,而是使用 AnyKey
注册的服务。
输出结果为:
(null)
(null)
为了验证不同键获取的服务实例是否为同一个对象,我们添加了以下代码:
Console.WriteLine("userIdProvider == envUserIdProvider ?? {0}", userIdProvider == envUserIdProvider);
输出结果为:
userIdProvider == envUserIdProvider ?? False
这表明不同的 serviceKey 获取的是不同的对象。
当 serviceKey
为 null
时,情况比较特殊。在当前的 API 设计中,虽然允许 serviceKey
为 null
,但实际上这会导致问题。例如:
var nullUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(null);
Console.WriteLine(nullUserIdProvider.GetUserId());
会抛出异常:
System.InvalidOperationException: No service for type 'Net8Sample.<__Script>FE1DBF3BE6F8384813B223E3EAA03DBABDC4153F95C5B3EBB0E0807E84E7C20E4__IUserIdProvider' has been registered.
这说明当 serviceKey
为 null
时,并不会像使用 AnyKey
那样获取服务,而是直接报错。并且,如果注册 keyed service 时使用 null
作为 serviceKey
,实际上相当于注册了一个非 keyed service。
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; }
}
在构造方法中,我们可以使用 ServiceKeyAttribute
来获取注册的 serviceKey
。在上述示例中,我们使用 KeyedService.AnyKey
注册服务,然后通过不同的键获取服务实例,并输出构造方法中获取的 serviceKey
。
Foo
Hello
这表明我们成功地在构造方法中获取到了实际使用的 serviceKey
。
需要注意的是,构造方法中的 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.
虽然需要类型一致,但 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.
这表明目前对于 scoped service 的支持存在问题。在 aspnetcore 中,基于 HttpContext.RequestServices
获取 keyedService 也会出现同样的问题,因为 HttpContext.RequestServices
是一个 scoped service provider。不过,已经有 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"));
通过结合 Options
,我们可以方便地实现基于选项的命名服务。在上述示例中,我们根据不同的键获取不同的 ITotpService
实例,并调用其 GetCode
方法。
Totp1: 356934
Totp2: 626994
KeyedService 解决了一些命名服务的痛点,让开发者可以更方便地按名称获取服务,减少了手动创建工厂类的工作量。结合 Options
使用时,还能实现更灵活的服务配置。
然而,目前该特性还存在一些问题,如 serviceKey
可以为 null
的设计不太合理,scoped service 支持存在 bug 等。不过考虑到这是预览版,这些问题是可以接受的,希望在正式版中能够得到妥善解决。
总体而言,KeyedService 是 .NET 8 中一个很有潜力的特性,为服务管理提供了新的思路和方法。开发者可以在项目中尝试使用,但在正式项目中使用时需要谨慎考虑其稳定性。 ======================================================================
前些天发现了一个比较好玩的人工智能学习网站,通俗易懂,风趣幽默,可以了解了解AI基础知识,人工智能教程,不是一堆数学公式和算法的那种,用各种举例子来学习,读起来比较轻松,有兴趣可以看一下。
人工智能教程