.NET 8 中的 KeyedService:新特性解析與使用示例
一、引言
在 .NET 8 的 Preview 7 版本中,引入了 KeyedService 支持。這一特性為開發者提供了按名稱(name)獲取服務的便利,在某些場景下,開發者無需再自行創建工廠類來管理服務。接下來,我們將深入探討 KeyedService 的使用方法、特殊情況以及存在的一些問題。
二、基本使用示例
1. 簡單示例代碼
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
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)
為了驗證不同鍵獲取的服務實例是否為同一個對象,我們添加了以下代碼:
Console.WriteLine("userIdProvider == envUserIdProvider ?? {0}", userIdProvider == envUserIdProvider);
輸出結果為:
userIdProvider == envUserIdProvider ?? False
這表明不同的 serviceKey 獲取的是不同的對象。
4. serviceKey 為 null 的情況
當 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。
四、構造方法中的 ServiceKeyAttribute
1. 示例代碼
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 類型和獲取服務時的類型應該保持一致,否則會拋出異常。例如:
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
類型,因此可以使用任意類型。例如:
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. 示例代碼及異常
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. 示例代碼
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基礎知識,人工智能教程,不是一堆數學公式和算法的那種,用各種舉例子來學習,讀起來比較輕松,有興趣可以看一下。
人工智能教程