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