如何在 .NET Core Web API 使用 DI ?
.NET Core 已經內建 DI,讓我們可以享受 DI container 實現 依賴注入 的方便,Visual Studio 2017 在 ASP.NET Core Web Application 的 template 中,預設已經可直接使用 DI,不需另外設定。
Version
Visual Studio 2017 15.4.5
.NET Core 2.0
Microsoft.Extensions.DependencyInjection 2.0.0
User Story
一個常見的分層架構,除了 OrderController 外,常見我們還會依 職責 拆分 OrderService 與 PaymentService … 等。
OrderController.Index() 呼叫 OrderService.CreateOrder(),然後 OrderService.CreateOrder() 再去呼叫 PaymentService.PayCreditCard()。
傳統我們會這樣寫 code :
OrderController.cs
1 | class OrderController |
OrderService.cs
1 | class OrderService |
PaymentService.cs
1 | class PaymentService |
這是傳統 OOP 寫法,但這有幾個問題 :
PaymentService直接被OrderServicenew在裡面,因此OrderController只要用了OrderService就必須得用PaymentService,完全沒有改變PaymentService的空間,也就是OrderController直接耦合PaymentService,無法替換PaymentService- 無法對
OrderService做單元測試,因為PaymentService直接new在OrderService內,無法對PaymentService做 mock
這就是典型的
耦合,也就是 高階模組 直接相依於低階模組,然後高階模組就被低階模組綁死了,無法抽換,也無法單元測試。
Task
目前 OrderController 與 PaymentService 直接偶合在一起,也就是用戶端只要用了 OrderController,就一定得使用 PaymentService,別無選擇。
現在用戶端希望使用 OrderController 時,還能決定 OrderController 的相依物件,不見的一定要 PaymentService,完全由用戶端決定,也就是對用戶端與 PaymentService 解耦合。
Architecture
OrderController、OrderService 與 PaymentService 已經符合 依賴反轉原則,也使用了 依賴注入,但要如何設定 依賴注入容器 呢 ?
Implementation
IPaymentService
IPayment.cs
1 | public interface IPaymentService |
因為 OrderService 為 PaymentService 的高階模組,由 OrderService 定義出 IPaymentService,由 PaymentService 所依賴。
PaymentService
PaymentService.cs
1 | public class PaymentService : IPaymentService |
低階模組 PaymentService 實現 (依賴) IPaymentService。
IOrderService
IOrderService.cs
1 | public interface IOrderService |
因為 OrderController 為 OrderService 的高階模組,由 OrderController 定義出 IOrderService,由 OrderService 所依賴。
OrderService
OrderService.cs
1 | public class OrderService : IOrderService |
低階模組 OrderService 實現 (依賴) IOrderService。
第 3 行
1 | private readonly IPaymentService paymentService; |
我們宣告了 IPaymentService 型別的 field,注意此為抽象型別 interface,而非具體型別,所以 OrderService 不會直接依賴 PaymentService,而是依賴 IPaymentService,符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象。
第 5 行
1 | public OrderService(IPaymentService paymentService) |
PaymentService 由 constructor 的參數傳進來,而不是直接 new 在 constructor 內。
如此 OrderService 的相依物件,就可透過 constructor 傳進來,從此高階模組 OrderService 就不再直接相依於低階模組 PaymentService,而是由更高階模組 OrderController 決定 OrderService 該依賴什麼物件,再由 constructor 參數傳進來,這就是 依賴注入。
注意 constructor 參數的型別是 IPaymentService,而不是 PaymentService,如此才會符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象。
我們改用 依賴注入 後有幾個優點 :
- 由原本相依於
PaymentService,改相依於IPaymentServiceinterface,符合依賴反轉原則 - 原本直接在 constructor 去
new,高階模組無法抽換低階模組,現在高階模組可透過 constructor 直接換掉低階模組,原來的OrderService程式碼不用變動,符合開放封閉原則 - 單元測試時,可直接透過 constructor 注入 mock 物件隔離測試
OOP 心法
若使用 DI 寫法,則不須再使用
new,讓程式碼更為乾淨,類似 static class 只要一行程式碼就可解決
10 行
1 | public string CreateOrder() |
由 CreateOrder() 去呼叫 PaymentService.PayCreditCard(),其中也因為要呼叫 PaymentService 的 PayCreditCard(),根據 界面隔離原則,因此 IPaymentService 有 PayCreditCard()。
OrderController
OrderController.cs
1 | using System; |
13 行
1 | private readonly IOrderService orderService; |
我們宣告了 IOrderService 型別的 field,注意此為抽象型別 interface,而非具體型別,所以 OrderController 不會直接依賴 OrderService,而是依賴 IOrderService,符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象。
15 行
1 | public OrderController(IOrderService orderService) |
OrderService 由 constructor 的參數傳進來,而不是直接 new 在 constructor 內。
如此 OrderController 的相依物件,就可透過 constructor 傳進來,從此高階模組 OrderController 就不再直接相依於低階模組 OrderService,而是由更高階模組決定 OrderController 該依賴什麼物件,再由 constructor 參數傳進來,這就是 依賴注入。
注意 constructor 參數的型別是 IOrderService,而不是 OrderService,如此才會符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象。
20 行
1 | // GET api/order |
當 GET 時,執行 OrderService.CreateOrder()。
Startup
Startup.cs
1 | using System; |
在 console app 時,若我們要使用 DI,還要自己建立 ServiceCollection 與 ServiceProvider,但在 ASP.NET Core,我們有更簡單的方法。
25 行
1 | public void ConfigureServices(IServiceCollection services) |
在 ASP.NET Core 要設定 DI 很簡單,不用自己建立 ServiceCollection 與 ServiceProvider,只要在 ConfigureServices() 下使用 AddTrasient() 建立 interface 與 class 的 mapping 即可。
AddTrasient(): 每次注入時,都重新new一個新的 instanceAddScoped(): 每個 request 都重新new一個新的instanceAddSingleton(): 程式啟動後會new一個 instance,之後會重複使用,也就是運行期間只有一個 instance
泛型參數部份 :
- 若只有 class,沒有 interface,如
OrderController:- 只須傳入第一個參數為 class
- 若有 interface 與對應 class
- 第一個參數為 interface
- 第二個參數為 class
也就是告訴 ServiceCollection,當 constructor 的型別為此 interface 時,請幫我 new 這個 class。
1 | public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services) |
其中 TImplementation 的 constraint 為 TService,也就是若你這樣寫
1 | services.AddTransient<IPaymentService, OrderService>(); |
因為 OrderService 根本沒有實作 IPaymentService,編譯會錯誤。
OOP 心法
在使用泛型時,一定要搭配 constraint,才會發揮泛型的威力
Conclusion
- ASP.NET Core 已經將 DI container 準備好,可直接使用
- DI 可以讓程式碼更乾淨,用起來類似 static 只要一行
- DI container 讓
依賴注入更容易實現,尤其在用戶端可以完全掌握低階模組的相依物件,完全符合依賴反轉原則
Sample Code
完整的範例可以在我的 GitHub 找到。
Reference
John Wu, ASP.NET Core 教學 - Dependency Injection
Microsoft, Introduction to Dependency Injection in ASP.NET Core
Joonas W, ASP.NET Core Dependency Injection Deep Dive
ASP.NET Hacker, Using Dependency Injection in .NET Core Console Apps