本文示例代码,均采用 .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>的註冊方法中。
