0%

C# 教育訓練 .NET core 依賴注入 (Dependency Injection)

.NET Core 支援依賴注入(Dependency Injection, DI)的設計模式,這是一種用於在類(Class)及其依賴項之間實現控制反轉(IoC)的技術。

依賴項注入允許從類的外部注入依賴項,因此注入依賴項的類只需要知道一個協定(通常是 C# 介面,Interface)。這個類可以遵守這個介面的協議而獨立於其物件的創建。

依賴注入並不一定需要依賴注入容器(Dependency Injection Container),但使用容器有助於管理依賴項。當依賴注入容器管理的服務列表越來越長時,就可以看到容器的優點。ASP.NET Core 和 Entity Framework Core 使用 Microsoft.Extensions.DependencyInjection 作為容器來管理所有的依賴項,以此管理數百個服務。所以王智民老師才會特別講解依賴注入。

儘管依賴注入和依賴注入容器在非常小的應用程序中會增加複雜性,但是一旦應用程序變得更大,需要多個服務,依賴注入就會顯示它的效益,降低應用程序的複雜性,並促進非緊密綁定的實現(鬆散耦合)。

到此,觀念還是很抽象,這裡就從一個不使用依賴注入的小應用程式開始;隨後將它轉換為使用依賴注入並使用注入依賴容器的應用程序。

使用沒有依賴注入的服務

開啟 VS Code,首先建立一個 .NET Core Console 專案

$ dotnet new console --name MyDI

使用 VS Code 切換到新建立的 .NET Core 專案目錄 MyDI。開啟一個終端螢幕輸入 dotnet run 測試一下。我們就可以開始了。

$ dotnet run
Hello World!

先在專案目錄下開兩個子目錄 Services 與 Controllers 分別擺放不同功能型態的程式碼。在 Services 下建立 GreetingService.cs,在 Controllers 下是 HomeController.cs。 程式碼都很短,不要用複製貼上,自己 Coding,享受一下 Coding 的樂趣。

這個 GreetingService 類定義了返回字符串的 Greet 方法。

Services/GreetingService.cs
1
2
3
4
5
6
7
namespace MyDI.Services
{
public class GreetingService
{
public string Greet(string name) => $"Hello Tainan, 哈囉,{name}";
}
}

HomeController 類使用了這個 GreetingService 服務。

Controllers/HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
using MyDI.Services;

namespace MyDI.Controllers
{
public class HomeController
{
public string Hello(string name)
{
var service = new GreetingService();
return service.Greet(name);
}
}
}

現在看看 Program 類的 Main( ) 方法。

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using MyDI.Controllers;

namespace MyDI
{
class Program
{
static void Main(string[] args)
{
var controller = new HomeController();
string result = controller.Hello("Emily");
Console.WriteLine(result);
}
}
}

現在跑跑看。

$ dotnet run

Hello Tainan, 哈囉,Emily

這有甚麼問題嗎? HomeController 與 GreetingService 是緊密耦合的。要用不同的實現取代 HomeController 中的 GreetingService 並不容易。

現在用另一個 SayHelloService 服務來取代原先的 GreetingService 看看會需要動到哪些程式碼。

在目錄 Services 下建立 SayHelloService 類,這裡用 SayHello 方法返回一個字符串。

Services/SayHelloService.cs
1
2
3
4
5
6
7
namespace MyDI.Services
{
public class SayHelloService
{
public string SayHello(string name) => $"Say 哈囉,{name}, Hello Tainan!";
}
}

要用 SayHelloService 取代 GreetingService 現在必需要直接修改 HomeController。

Controllers/HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
using MyDI.Services;

namespace MyDI.Controllers
{
public class HomeController
{
public string Hello(string name)
{
var service = new SayHelloService();
return service.SayHello(name);
}
}
}

這裡修改了第 9 與 10 行,改使用 SayHelloService 服務。

$ dotnet run

Say 哈囉,Emily, Hello Tainan!

在這個簡單的例子中看起來也還好,並不是很複雜。 但這裡的服務都只是返回字符串的簡單服務,一般的應用程序通常更複雜,要修改的也更多,有些還會有一些相互的牽扯,最怕的是有些還不知道藏在哪裡,要等到出問題才會知道。現在改用依賴注入,看看會有甚麼不同。

使用依賴注入

現在要讓 HomeController 獨立於 GreetingService 與 SayHelloService 的實現,當要變更服務時,不用再直接修改 HomeController 類。為此,我們必須訂立一個協定,讓這些服務遵守,這裡要用一個 IGreetingService 介面(Interface)來定義 HomeController 所需要的功能,IGreetingService 介面就是一個協定。在專案目錄下建一個子目錄 Interfaces 擺放介面程式碼 IGreetingService.cs

Interfaces/IGreetingService.cs
1
2
3
4
5
6
7
namespace MyDI.Interfaces
{
public interface IGreetingService
{
string Greet(string name);
}
}

在這個 IGreetingService 介面中,目前只有一個協議 Greet 方法。現在 GreetingService 與 SayHelloService 都必須遵守這個協定,也就是都要實現這個 Greet 方法。

Services/GreetingService.cs
1
2
3
4
5
6
7
8
9
using MyDI.Interfaces;

namespace MyDI.Services
{
public class GreetingService : IGreetingService
{
public string Greet(string name) => $"Hello Tainan, 哈囉,{name}";
}
}
Services/SayHelloService.cs
1
2
3
4
5
6
7
8
9
10
11
using MyDI.Interfaces;

namespace MyDI.Services
{
public class SayHelloService : IGreetingService
{
public string Greet(string name) => $"Say 哈囉,{name}, Hello Tainan!";

public string SayHello(string name) => $"Say 哈囉,{name}, Hello Tainan!";
}
}

SayHelloService 原先並沒有 Greet 方法,因此必須實作,這裡將舊的 SayHello( ) 方法留著,萬一有人使用了緊密耦合。

HomeController 現在只需要對一個物件的引用,該物件會實現介面 IGreetingService 中的所有協議。這個物件會用 HomeController 的建構式函式(Constructor)注入,分配給私有字段(private filed),通過方法 Hello 來使用。

Controllers/HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using MyDI.Interfaces;

namespace MyDI.Controllers
{
public class HomeController
{
private readonly IGreetingService _greetingService;

public HomeController(IGreetingService greetingService) {
_greetingService = greetingService ?? throw new ArgumentNullException(nameof(greetingService));
}

public string Hello(string name) => _greetingService.Greet(name);
}
}

第 10 行是 HomeController 的建構函式,參數型態是 IGreetingService 介面,因此只要能實現 IGreetingService 介面的物件都可以由此注入。HomeController 則透過 Hello 方法來使用這個注入物件的 Greet 方法。 

在這個實現中,HomeController 類利用了控制反轉(Inversion of Control, IoC)的設計原理。HomeController 並沒有像以前那樣直接實例化 GreetingService 或 SayHelloService。相反的,定義由 HomeController 使用具體類的控件從外部注入,也就是透過 HomeController 的建構函式從外部注入。換句話說,控制是反轉的。HomeController 不能直接決定自己要用哪個服務,而是由外界決定 HomeController 該用哪個服務。反轉控制也被稱為好萊塢原則:不要給我們打電話,我們會給你打電話(don’t call us, we’ll call you)。

HomeController 類並沒有依賴 IGreetingService 介面的具體實作,而是可以使用實現了介面 IGreetingService 的任何類。所以這裡可以使用 GreetingService 與 SayHelloService 類,因這兩個類都具體實作了 IGreetingService 介面。

現在,需要從外部注入依賴項,將具體的實現傳遞給 HomeController 類的建構函式。

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using MyDI.Controllers;
using MyDI.Services;

namespace MyDI
{
class Program
{
static void Main(string[] args)
{
var controller = new HomeController(new GreetingService());
string result = controller.Hello("Emily");
Console.WriteLine(result);
}
}
}

第 11 行使用建構函式注入的依賴注入模式實現了控制反轉的設計原則。這稱為建構函式注入,因為介面是在建構函式中注入的。所以這裡需要注入一個依賴項物件來創建一個 HomeController 實例。

$ dotnet run

Hello Tainan, 哈囉,Emily

現在注入不同的服務,更改第 11 行注入不同的依賴項:

Program.cs
1
2
3
...
var controller = new HomeController(new SayHelloService());
...
$ dotnet run

Say 哈囉,Emily, Hello Tainan!

到此,我們已完成了依賴注入的改寫。這個範例程序非常小,唯一需要注入的是一個實現協定 IGreetingService 介面的類。 這個類在實例化的同時實例化了 HomeController。

到目前為止我們已經實現了依賴注入,但還沒有使用容器來管理這些依賴項。在實際應用程序中,需要處理許多介面與實現,還需要共享實例,最好的方法還是使用依賴注入容器來管理所有的依賴項。

使用 .NET Core DI 容器

依賴注入不一定需要使用依賴注入容器,但容器有助於管理依賴項,這裡要使用 Microsoft.Extensions.DependencyInjection 作為容器來管理所有依賴項。

首先需要安裝 Microsoft.Extensions.DependencyInjection。

$ dotnet add package Microsoft.Extensions.DependencyInjection

在依賴注入容器中,可以在應用程式中有一個位置,在其中定義甚麼協定(介面 Interface)映射到哪個特定的實現(具體的類 Class)上。還可以指定是應該將服務作為一個單例(Singleton)來使用,還是應該在每次使用時創建一個新實例(Transient)。

首先在 Program 類中定義 RegisterService 方法。在這裡,實例化一個新的 ServiceCollection 物件,ServiceCollection 就在 Microsoft.Extensions.DependencyInjection 名稱空間中,使用 ServiceCollection 的擴展方法 AddSingleton 與 AddTransient 來註冊 DI 容器需要知道的類型,範例中我們將 GreetingService 與 HomeController 都註冊在容器中,這樣允許從容器中檢索 HomeController。

當請求 IGreetingService 介面時,會實例化 GreetingService 類。HomeController 本身沒有實現介面。通過這個 DI 容器配置,當請求 HomeController 時,DI 容器會時例化 HomeController。

DI 容器配置還定義了服務的生命週期,使用 AddSingleton 與 AddTransient 方法指定服務的生命週期信息。對於 GreetingService,使用 AddSingleton 註冊,因此請求 IGreetingService 時總是返回相同的實例。這和 HomeController 不同, HomeControler,使用 AddTransient 註冊,每次請求檢索 HomeController 時,則都會創建一個新的實例。

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using Microsoft.Extensions.DependencyInjection;
using MyDI.Controllers;
using MyDI.Services;
using MyDI.Interfaces;

namespace MyDI
{
class Program
{
static void Main(string[] args)
{
using (ServiceProvider container = RegisterServices())
{
var controller = container.GetRequiredService<HomeController>();
string result = controller.Hello("Emily");
Console.WriteLine(result);
}
}

static ServiceProvider RegisterServices() {
var services = new ServiceCollection();
services.AddSingleton<IGreetingService, GreetingService>();
services.AddTransient<HomeController>();
return services.BuildServiceProvider();
}
}
}

第 21 行新增靜態方法 RegisterService( )。
第 23、24 行註冊 GreetingService 與 HomeController。
第 25 行調用 BuildServiceProvider,這會返回一個 ServiceProvider 物件,該物件可以用來訪問已註冊的服務。

再來要修改 Main 方法來調用 RegisterService 方法以便在容器中註冊。

第 13 行允許直接訪問 ServiceProvider 的 Dispose 方法,所以這裡使用 using 語句。
第 15 行調用 ServiceProvider 的 GetRequiredService 方法來取得對 HomeController 實例的引用。

啟動應用程序時,在 GetRequiredService 方法的請求下,DI 容器將創建 HomeController 類的實例。HomeController 的建構函式需要一個實現 IGreetingService 的物件。這個介面 IGreetingService 也有在容器中註冊,在此它會返回 GreetingService 物件。GreetingService 類有一個預設的建構函式,因此容器可以創建一個實例,並將該實例傳遞給 HomeController 的建構函式。

$ dotnet run

Hello Tainan, 哈囉,Emily

現在改註冊另外一個 SayHelloService 服務。

Program.cs
1
2
3
4
5
6
7
8
9
 ...
static ServiceProvider RegisterServices() {
var services = new ServiceCollection();
// services.AddSingleton<IGreetingService, GreetingService>();
services.AddSingleton<IGreetingService, SayHelloService>();
services.AddTransient<HomeController>();
return services.BuildServiceProvider();
}
...
$ dotnet run

Say 哈囉,Emily, Hello Tainan!

如果同時註冊 GreetingService 與 SayHelloService 那又會如何? 如果多次將相同的介面協議添加到服務集合(ServiceCollection)中,最後一個註冊的介面協議就會從容器中獲得介面。

ASP.NET Core Startup

ASP.NET Core 提供內建服務容器 IServiceProvider。服務會在應用程式的 Startup.ConfigureServices 方法中註冊。

ASP.NET Core 的應用程式啟動使用 Startup 類別,其依慣例命名為 Startup。 Startup 類別可設定服務和應用程式的要求管線。

Startup 類別選擇性地包含 ConfigureServices 方法來設定應用程式的服務,服務透過依賴項注入 (DI),在應用中使用 ConfigureServices 來註冊和使用。

另外 Startup 類也包含 Configure 方法來建立應用程式的要求處理管線(Pipeline,Middlewares)。

瞭解了依賴注入 (Dependency Injection) 與依賴注入容器,應該可以更理解王智民老師的教育訓練課程中的有關 MVC 控制器內依賴注入。

祝,Coding 快樂!