本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。
透過前面的文章,了解到了服務的三種聲明週期。那麼如果我們需要註冊服務時,框架都提供了哪些 API 呢?當我們要根據自身需求來聲明服務,聲明具體的實現時,又該如何編寫代碼呢?本文將探討 .NET 內置的 DI 框架提供的 API,以及其使用方法,具體作用。
.NET API 瀏覽器,提供了詳細的 API 文檔。這個鏈接就展示了註冊服務相關的 API。
註冊方法#
註冊服務的方法,肯定不止Add{LifeTime}<TService, TImplementation>()
這一個方法,具體的方法類型,可以見點擊這個鏈接,查看文檔提供的表格,我在下面直接將其粘貼出來。
方法 | 自動釋放 | 多種實現 | 傳遞參數 |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | 是 | 是 | 否 |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | 是 | 是 | 是 |
Add{LIFETIME}<{IMPLEMENTATION}>() | 是 | 否 | 否 |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | 否 | 是 | 是 |
AddSingleton(new {IMPLEMENTATION}) | 否 | 否 | 是 |
上面的表格,只列出了支持泛型的方法,還有形如Add{LIFETIME}(typeof({SERVICE}), typeof({IMPLEMENTATION}))
的方法,其本質和泛型方法一樣,這裡就不列出來了。表格中右邊三列,分別描述了這些方法的局限範圍。
服務釋放#
容器負責服務的構建,但是如果一個服務到期,又是誰來負責服務的釋放?是服務的依賴者?還是容器?
前面表格中的自動釋放
一列,如果為是
,則表明通過這類方法註冊的服務,由容器統一進行服務的釋放,如果這些服務中有實現IDisposable
接口,那麼也由容器來自動調用Dispose
方法,無需在代碼中顯式調用釋放方法。
查看表格,可以看到AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
和AddSingleton(new {IMPLEMENTATION})
兩類方法中,是不會自動釋放的,容器框架不會去自行釋放,那麼就需要開發人員自行負責服務的釋放。
多種實現#
前面的例子中,我們註冊時都是一個接口,一個實現類,在依賴函數注入時,注入的都是單個服務。如果我們想要將多個實現,都註冊為同一種接口,又該使用哪些方法呢?
前面表格中可以看到在多種實現
這一列,有兩類方法不支持多種實現,Add{LIFETIME}<{IMPLEMENTATION}>()
和AddSingleton(new {IMPLEMENTATION})
。其實查看方法描述就可以了解到,前者註冊時,直接將IMPLEMENTATION
類的實現,註冊為IMPLEMENTATION
類型的服務,沒法做到多種實現
。而後者也是類似,直接將一個註冊實例,自然也沒法做到多種實現
。
這裡的多種實現
,指的是實現了同一種接口的多個實現類,在註冊服務的時候,同時註冊到這個接口上。在上一篇文章中,我們是用一個實現類實現了多種接口,在註冊服務的時候,這個類同時註冊了多個接口,這兩種情況是不一樣的,後者不算多種實現
。
接下來我們直接使用代碼,來展示如何做到多種實現
:
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService4
// 1. 設定一個接口 IMyDependency,以及實現這個接口的多個實現類
public interface IMyDependency
{
string Id { get; }
bool Flag { get; }
}
public class MyDependency0 : IMyDependency
{
public string Id { get; } = Guid.NewGuid().ToString()[^4..];
public bool Flag { get; init; } = false;
}
public class MyDependency1 : MyDependency0 {}
public class MyDependency2 : MyDependency0 {}
public class MyDependency3 : MyDependency0 {}
public class MyDependency4 : MyDependency0 {}
// 2. 註冊服務
using Microsoft.Extensions.DependencyInjection.Extensions;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// 同時註冊一個接口的多種實現
services.AddTransient<IMyDependency, MyDependency1>();
services.AddTransient<IMyDependency, MyDependency2>();
// TryAdd 如果接口已被註冊過,不會再註冊
// IMyDependency 已經被註冊過,所以並不會將 MyDependency3 註冊為它的實現
services.TryAddTransient<IMyDependency, MyDependency3>();
// TryAddEnumerable 如果接口的同一種實現被註冊過,則不會重複註冊
// IMyDependency -> MyDependency4,這個關係沒有被註冊過,所以可以成功註冊
var descriptor = new ServiceDescriptor(
typeof(IMyDependency),
typeof(MyDependency4),
ServiceLifetime.Transient);
services.TryAddEnumerable(descriptor);
// 不會檢查是否註冊過這個接口,也不會檢查是否註冊過這個關係,會重複註冊
// MyDependency2 被註冊過兩次
// 那麼在獲取 IEnumerable<IMyDependency> 時
// MyDependency2 類型的實例會有兩個
services.AddTransient<IMyDependency, MyDependency2>(_ => new MyDependency2
{
Flag = true // 單獨使用工廠方法構造這個服務,用於區分最後一次註冊的服務
});
})
.Build();
Fun(host.Services);
static void Fun(IServiceProvider serviceProvider)
{
using var scopeServices = serviceProvider.CreateScope();
var services = scopeServices.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
GetData(myDependency);
Console.WriteLine();
var list = services.GetRequiredService<IEnumerable<IMyDependency>>();
foreach (var dependency in list)
{
GetData(dependency);
}
Console.WriteLine();
}
static void GetData<T>(T item) where T : IMyDependency
{
Console.WriteLine($"{item.GetType().Name} {item.Id}, {item.Flag}");
}
程序的運行結果如下:
MyDependency2 c432, True
MyDependency1 ea48, False
MyDependency2 9b9a, False
MyDependency4 c4ce, False
MyDependency2 77e9, True
查看整個程序的輸出結果,因為將所有服務註冊為Transient
類型的聲明週期,所以每個實例的 Id 都不一樣。
查看第一行輸出中,item.Flag 為True
,也就是說在解析IMyDependency
類型時,最後一次成功註冊的實現類將會被構造並注入(即本例中的myDependency
變量)。
對比下面四行輸出,其順序正好就是服務註冊時的順序,即獲取IEnumerable<IMyDependency>
類型的變量時,會按照註冊順序,將所有成功註冊的類型,統統注入進去。而這裡面的第二行和第四行,都是MyDependency2
,說明註冊服務,是可以重複註冊同一實現的。
傳遞參數#
方法 | 自動釋放 | 多種實現 | 傳遞參數 |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | 是 | 是 | 否 |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | 是 | 是 | 是 |
Add{LIFETIME}<{IMPLEMENTATION}>() | 是 | 否 | 否 |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | 否 | 是 | 是 |
AddSingleton(new {IMPLEMENTATION}) | 否 | 否 | 是 |
傳遞參數其實就是容器在構造服務實例時,是否可以使用工廠方法,使用多樣化的方式構造實例,而非單純調用構造函數。查看這個表格,若是傳遞了工廠方法的,其傳遞參數
列都為是
,反之則為否
。實際上,即使不傳遞工廠方法,也可以用其他途徑,做到傳遞參數的目的。
比如在構造函數中使用IOperation<>
類型。將需要的配置數據,註冊到IOperation<MyOperation>
中,就可以在構造函數中,拿到需要的實時數據,從而達到傳遞參數的目的。而具體配置數據的獲取,則可以放在IOperation<MyOperation>
的註冊方法中。