ASP.NET Core - 依賴注入(一)

啊晚發表於2023-02-21

1. Ioc 與 DI

Ioc 和DI 這兩個詞大家都應該比較熟悉,這兩者已經在各種開發語言各種框架中普遍使用,成為框架中的一種基本設施了。

Ioc 是控制反轉, Inversion of Control 的縮寫,DI 是依賴注入,Inject Dependency 的縮寫。

所謂控制反轉,反轉的是類與類之間依賴的建立。型別A依賴於型別B時,不依賴於具體的型別,而是依賴於抽象,不在類A中直接 new 類B的物件,而是透過外部傳入依賴的型別物件,以實現類與類之間的解耦。所謂依賴注入,就是由一個外部容器對各種依賴型別物件進行管理,包括物件的建立、銷燬、狀態保持等,並在某一個型別需要使用其他型別物件的時候,由容器進行傳入。

下圖是一張網圖,是關於Ioc解耦比較經典的圖示過程了。至於依賴解耦的好處,就不在這裡細講了,如果有對依賴注入基本概念不理解的,可以稍微搜尋一下相關的文章,也可以參考 ASP.NET Core 依賴注入 | Microsoft Learn 官方文件中的講解。

image

Ioc是一種設計思想,而DI是這種思想的具體實現。依賴注入是一種設計模式,是對物件導向程式設計五大基本原則中的依賴倒置原則的實踐,其中很重要的一個點就是 Ioc 容器的實現。

2. .NET Core 依賴注入的基本用法

在 .NET Core 平臺下,有一套自帶的輕量級Ioc框架,如果是ASP.NET Core專案,更是在使用主機的時候自動整合了進去,我們在startup類中的ConfigureServices方法中的程式碼就是往容器中配置依賴注入關係,如果是控制檯專案的話,還需要自己去整合。除此之外,.NET 平臺下還有一些第三方依賴注入框架,如 Autofac、Unity、Castle Windsor等。

這裡先不討論第三方框架的內容,先簡單介紹一下.Net Core平臺自帶的Ioc框架的使用。

2.1 服務

依賴項注入術語中,服務通常是指向其他物件提供服務的物件,既可以作為其他類的依賴項,也可能依賴於其他服務。服務是Ioc容器管理的物件。

2.2 服務生命週期

使用了依賴注入框架之後,所有我們注入到容器中的型別的建立、銷燬工作都由容器來完成,那麼容器什麼時候建立一個型別例項,什麼時候銷燬一個型別例項呢?這就涉及到注入服務的生命週期了。根據我們的需要,我們可以向容器中註冊服務的時候,對服務的生命週期進行設定。服務的生命週期有以下三種:

(1) 單例 Singleton

註冊成單例模式的服務,整個應用程式生命週期以內只建立一個例項。在應用內第一個使用到該服務時建立,在應用程式停止時銷燬。
在某些情況下,對於某些特殊的類,我們需要註冊成單例模式,這可以減少例項初始化的消耗,還能實現跨 Service 事務的功能。

(2)範圍(或者作用域) Scoped

在同一個範圍內只初始化一個例項 。在 web 應用中,可以理解為每一個 request 級別只建立一個例項,同一個 http request 會在一個 scope 內。

(3)多例 Tranisent

每一次使用到服務時都會建立一個新的例項,每一次對該依賴的獲取都是一個新例項。

2.3 服務註冊

在ASP.NET Core這樣的web應用框架中,在使用主機的時候就自動整合了依賴注入框架,之後我們可以透過 IServiceCollection 物件來註冊依賴注入關係。前面入口檔案一篇講過,.NET 6 之前可以在 Startup 類中的 ConfigureServices 方法中進行註冊,該方法傳入IServiceCollection引數,.NET 6 之後,可以透過 WebApplicationBuilder 物件的 Services屬性進行註冊。

服務註冊常用的方法如下:

  • Add 方法
    透過引數 ServiceDescriptor 將服務型別、實現型別、生命週期等資訊傳入進去,是服務註冊最基本的方法。其中 ServiceDescriptor 引數又有多種變形。

    // 最基本的服務註冊方法,除此之外還有其他各種變形
    builder.Services.Add(new ServiceDescriptor(typeof(IRabbit), typeof(Rabbit), ServiceLifetime.Transient));
    builder.Services.Add(ServiceDescriptor.Scoped<IRabbit, Rabbit>());
    builder.Services.Add(ServiceDescriptor.Singleton(typeof(IRabbit), (services) => new Rabbit()));
    
  • Add{lifetime}擴充套件方法
    基於 Add 方法的擴充套件方法,包括以下幾種,每種都有多個過載:

    // 基於生命週期的擴充套件方法,以下為例項,正式開發中不可能將一個型別註冊為多個生命週期,會丟擲異常
    builder.Services.AddTransient<IRabbit, Rabbit>();
    builder.Services.AddTransient(typeof(IRabbit), typeof(Rabbit));
    builder.Services.AddScoped<IRabbit, Rabbit>();
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    
  • TryAdd{lifetime}擴充套件方法
    對於 Add{lifetime} 方法的擴充套件,位於名稱空間 Microsoft.Extensions.DependencyInjection.Extensions 下。
    與 Add{lifetime} 方法相比,差別在於當使用 Add{lifetime} 方法將同樣的服務註冊了多次時,在使用 IEnumerable<{Service}> 解析服務時,就會產生多個例項的副本,這可能會導致一些意料之外的 bug,特別是單例生命週期的服務。

    // 同一個服務同一個實現注入多次
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    builder.Services.AddSingleton<IRabbit, Rabbit>();
    
    [ApiController]
    [Route("[controller]")]
    public class InjectTestController : ControllerBase
    {
    	private readonly IEnumerable<IRabbit> _rabbits;
    	public InjectTestController(IEnumerable<IRabbit> rabbits)
    	{
    		_rabbits = rabbits;
    
    	[HttpGet("")]
    	public Task InjectTest()
    	{
    		// 2個IRabbit例項
    		Console.WriteLine(_rabbits.Count());
    		var rabbit1 = _rabbits.First();
    		var rabbit2 = _rabbits.ElementAt(1);
    		// 都是 Rabbit 型別
    		Console.WriteLine(rabbit1 is Rabbit);
    		Console.WriteLine(rabbit2 is Rabbit);
    		// 兩個例項不是同一個
    		Console.WriteLine(rabbit1 == rabbit2);
    		return Task.CompletedTask;
    	}
    }
    

    呼叫介面後,列印輸出結果如下:

    image

    而使用 TryAdd{lifetime} 方法,當DI容器中已存在指定型別的服務時,則不進行任何操作;反之,則將該服務注入到DI容器中。

    將服務註冊改成以下程式碼:

    builder.Services.AddTransient<IRabbit, Rabbit>();
    // 由於上面已經註冊了服務型別 IRabbit,所以下面的程式碼不不會執行任何操作(與生命週期無關)
    builder.Services.TryAddTransient<IRabbit, Rabbit>();
    builder.Services.TryAddTransient<IRabbit, Rabbit1>();
    

    在上面的控制器中新增以下方法:

    [HttpGet(nameof(InjectTest1))]
    public Task InjectTest1()
    {
        // 只有1個IRabbit例項
        Console.WriteLine(_rabbits.Count());
        var rabbit1 = _rabbits.First();
        // 都是 Rabbit 型別
        Console.WriteLine(rabbit1 is Rabbit);
        return Task.CompletedTask;
    }
    

    呼叫介面後,列印輸出結果如下:
    image

  • TryAddEnumerable 方法

    與 TryAdd 對應,區別在於TryAdd僅根據服務型別來判斷是否要進行註冊,而TryAddEnumerable則是根據服務型別和實現型別一同進行判斷是否要進行註冊,常常用於註冊同一服務型別的多個不同實現。

    將服務註冊改成以下程式碼:

    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>());
    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 未進行任何操作,因為 IRabbit 服務的 Rabbit實現在上面已經註冊了
    builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>());
    

    在上面的控制器新增一個方法:

    [HttpGet(nameof(InjectTest2))]
    public Task InjectTest2()
    {
        // 2個IRabbit例項
        Console.WriteLine(_rabbits.Count());
        var rabbit1 = _rabbits.First();
        var rabbit2 = _rabbits.ElementAt(1);
        // 第一個是 Rabbit 型別,第二個是 Rabbit1型別
        Console.WriteLine(rabbit1 is Rabbit);
        Console.WriteLine(rabbit2 is Rabbit1);
        return Task.CompletedTask;
    }
    

    呼叫介面後,控制檯列印如下:
    image

  • Repalce 與 Remove 方法

    當我們想要從Ioc容器中替換或是移除某些已經註冊的服務時,可以使用Replace和Remove。

    // 將容器中註冊的IRabbit實現替換為 Rabbit1
    builder.Services.Replace(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 從容器中 IRabbit 註冊的實現 Rabbit1
    builder.Services.Remove(ServiceDescriptor.Transient<IRabbit, Rabbit1>());
    // 移除 IRabbit服務的所有註冊
    builder.Services.RemoveAll<IRabbit>();
    // 清空容器中的所有服務註冊
    builder.Services.Clear();
    

以上是 .NET Core 框架自帶的 Ioc 容器的一些基本概念和依賴關係注入的介紹,下一章是注入到容器中的服務使用部分。



參考文章:
ASP.NET Core 依賴注入 | Microsoft Learn
理解ASP.NET Core - 依賴注入(Dependency Injection)



ASP.NET Core 系列:
目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 自定義中介軟體

相關文章