Ioc 和DI 這兩個詞大家都應該比較熟悉,這兩者已經在各種開發語言各種框架中普遍使用,成為框架中的一種基本設施了。
Ioc 是控制反轉, Inversion of Control 的縮寫,DI 是依賴注入,Inject Dependency 的縮寫。
所謂控制反轉,反轉的是類與類之間依賴的建立。型別A依賴於型別B時,不依賴於具體的型別,而是依賴於抽象,不在類A中直接 new 類B的物件,而是通過外部傳入依賴的型別物件,以實現類與類之間的解耦。所謂依賴注入,就是由一個外部容器對各種依賴型別物件進行管理,包括物件的建立、銷燬、狀態保持等,並在某一個型別需要使用其他型別物件的時候,由容器進行傳入。
下圖是一張網圖,是關於Ioc解耦比較經典的圖示過程了。至於依賴解耦的好處,就不在這裡細講了,如果有對依賴注入基本概念不理解的,可以稍微搜尋一下相關的文章,也可以參考 ASP.NET Core 依賴注入 | Microsoft Learn 官方檔案中的講解。
Ioc是一種設計思想,而DI是這種思想的具體實現。依賴注入是一種設計模式,是對物件導向程式設計五大基本原則中的依賴倒置原則的實踐,其中很重要的一個點就是 Ioc 容器的實現。
在 .NET Core 平臺下,有一套自帶的輕量級Ioc框架,如果是ASP.NET Core專案,更是在使用主機的時候自動整合了進去,我們在startup類中的ConfigureServices方法中的程式碼就是往容器中設定依賴注入關係,如果是控制檯專案的話,還需要自己去整合。除此之外,.NET 平臺下還有一些第三方依賴注入框架,如 Autofac、Unity、Castle Windsor等。
這裡先不討論第三方框架的內容,先簡單介紹一下.Net Core平臺自帶的Ioc框架的使用。
依賴項注入術語中,服務通常是指向其他物件提供服務的物件,既可以作為其他類的依賴項,也可能依賴於其他服務。服務是Ioc容器管理的物件。
使用了依賴注入框架之後,所有我們注入到容器中的型別的建立、銷燬工作都由容器來完成,那麼容器什麼時候建立一個型別範例,什麼時候銷燬一個型別範例呢?這就涉及到注入服務的生命週期了。根據我們的需要,我們可以向容器中註冊服務的時候,對服務的生命週期進行設定。服務的生命週期有以下三種:
(1) 單例 Singleton
註冊成單例模式的服務,整個應用程式生命週期以內只建立一個範例。在應用內第一個使用到該服務時建立,在應用程式停止時銷燬。
在某些情況下,對於某些特殊的類,我們需要註冊成單例模式,這可以減少範例初始化的消耗,還能實現跨 Service 事務的功能。
(2)範圍(或者作用域) Scoped
在同一個範圍內只初始化一個範例 。在 web 應用中,可以理解為每一個 request 級別只建立一個範例,同一個 http request 會在一個 scope 內。
(3)多例 Tranisent
每一次使用到服務時都會建立一個新的範例,每一次對該依賴的獲取都是一個新範例。
在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;
}
}
呼叫介面後,列印輸出結果如下:
而使用 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;
}
呼叫介面後,列印輸出結果如下:
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;
}
呼叫介面後,控制檯列印如下:
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 - 自定義中介軟體