.NET 8 中的 KeyedService

.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 的情况

serviceKeynull 时,情况比较特殊。在当前的 API 设计中,虽然允许 serviceKeynull,但实际上这会导致问题。例如:

csharp 复制代码
var nullUserIdProvider = services.GetRequiredKeyedService<IUserIdProvider>(null);
Console.WriteLine(nullUserIdProvider.GetUserId());

会抛出异常:

复制代码
System.InvalidOperationException: No service for type 'Net8Sample.<__Script>FE1DBF3BE6F8384813B223E3EAA03DBABDC4153F95C5B3EBB0E0807E84E7C20E4__IUserIdProvider' has been registered.

这说明当 serviceKeynull 时,并不会像使用 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 类型的灵活性

虽然需要类型一致,但 serviceKeyobject 类型,因此可以使用任意类型。例如:

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基础知识,人工智能教程,不是一堆数学公式和算法的那种,用各种举例子来学习,读起来比较轻松,有兴趣可以看一下。
人工智能教程

相关推荐
ChinaRainbowSea2 小时前
用户中心——比如:腾讯的QQ账号可以登录到很多应用当中 02
java·服务器·spring boot·后端·mysql·mybatis
追逐时光者3 小时前
精选 10 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
青竹易寒3 小时前
Linux命令技术笔记-sed+awk命令详解
linux·运维·服务器
试着4 小时前
零基础学习性能测试第二章-linux服务器监控:CPU监控
linux·服务器·学习·零基础·性能测试·cpu监控
绵绵细雨中的乡音4 小时前
Linux进程通信——共享内存:System V 进程间通信的极速方案
linux·运维·服务器
Littlewith4 小时前
Node.js:Stream、模块系统
java·服务器·开发语言·node.js·编辑器
Gappsong8745 小时前
浅析网络安全面临的主要威胁类型及对应防护措施
运维·网络·安全·web安全·网络安全
Kookoos5 小时前
ABP VNext + Temporal:分布式工作流与 Saga
.net·temporal·abp vnext·continue-as-new
<但凡.5 小时前
Git 完全手册:从入门到团队协作实战(2)
服务器·git·bash