[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入 (Dependency Injection)

-- Pageviews

ASP.NET Core 使用了大量的依賴注入 (Dependency Injection, DI),把控制翻轉 (Inversion Of Control, IoC) 運用的相當落實。DI 可算是 ASP.NET Core 最精華的一部分,有用過 Autofac 或類似的 DI Framework 對此應該不陌生。
本篇將介紹 ASP.NET Core 的依賴注入(Dependency Injection)。

iT 邦幫忙 2018 鐵人賽 - Modern Web 組參賽文章:
[Day04] ASP.NET Core 2 系列 - 依賴注入 (Dependency Injection)

DI 容器介紹

在沒有使用 DI Framework 的情況下,假設在 UserController 要呼叫 UserLogic,會直接在 UserController 實例化 UserLogic,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserLogic {
public void Create(User user) {
// ...
}
}

public class UserController : Controller {
public void Register(User user){
var logic = new UserLogic();
logic.Create(user);
// ...
}
}

xxxLogic 邏輯層分層命名,有興趣可以參考這篇:軟體分層架構模式

以上程式基本上沒什麼問題,但程式相依性就差了點。UserController 必須 要依賴 UserLogic 才可以運作,就算拆出介面改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IUserLogic {
void Create(User user);
}

public class UserLogic : IUserLogic {
public void Create(User user) {
// ...
}
}

public class UserController : Controller {
private readonly IUserLogic _userLogic;

public UserController() {
_userLogic = new UserLogic();
}

public void Register(User user){
_userLogic.Create(user);
// ...
}
}

UserController 與 UserLogic 的相依關係只是從 Action 移到建構子,依然還是很強的相依關係。

ASP.NET Core 透過 DI 容器,切斷這些相依關係,實例的產生不會是在使用方(指上例 UserController 建構子的 new),而是在 DI 容器。
DI 容器的註冊方式也很簡單,在 Startup.ConfigureServices 註冊。如下:

Startup.cs

1
2
3
4
5
6
7
8
// ...
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
}

services 就是一個 DI 容器。
此例把 MVC 的服務註冊到 DI 容器,等到需要用到 MVC 服務時,才從 DI 容器取得物件實例。

基本上要注入到 Service 的類別沒什麼限制,除了靜態類別。
以下範例程式就只是一般的 Class 繼承 Interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface ISample
{
int Id { get; }
}

public class Sample : ISample
{
private static int _counter;
private int _id;

public Sample()
{
_id = ++_counter;
}

public int Id => _id;
}

要注入的 Service 需要在 Startup.ConfigureServices 中註冊實做類別。如下:

Startup.cs

1
2
3
4
5
6
7
8
9
// ...
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<ISample, Sample>();
}
}
  • 第一個泛型為注入的類型
    建議用 Interface 來包裝,這樣在才能把相依關係拆除。
  • 第二個泛型為實做的類別

DI 運作方式

ASP.NET Core 的 DI 是採用 Constructor Injection,也就是說會把實例化的物件從建構子傳入。
如果要取用 DI 容器內的物件,只要在建構子加入相對的 Interface 即可。例如:

Controllers\HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HomeController : Controller
{
private readonly ISample _sample;

public HomeController(ISample sample)
{
_sample = sample;
}

public string Index() {
return $"[ISample]\r\n"
+ $"Id: {_sample.Id}\r\n"
+ $"HashCode: {_sample.GetHashCode()}\r\n"
+ $"Tpye: {_sample.GetType()}";
}
}

輸出內容如下:

1
2
3
4
[ISample]
Id: 1
HashCode: 14145203
Tpye: MyWebsite.Sample

ASP.NET Core 實例化 Controller 時,發現建構子有 ISample 這個類型的參數,就把 Sample 的實例注入給該 Controller。

每個 Request 都會把 Controller 實例化,所以 DI 容器會從建構子注入 ISample 的實例,把 sample 存到欄位 _sample 中,就能確保 Action 能夠使用到被注入進來的 ISample 實例。

注入實例過程,情境如下:

[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入(Dependency Injection) - 注入實例

Service 生命週期

註冊在 DI 容器的 Service 有分三種生命週期:

  • Transient
    每次注入時,都重新 new 一個新的實例。
  • Scoped
    每個 Request 都重新 new 一個新的實例,同一個 Request 不管經過多少個 Pipeline 都是用同一個實例。上例所使用的就是 Scoped
  • Singleton
    被實例化後就不會消失,程式運行期間只會有一個實例。

小改一下 Sample 類別的範例程式:

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
29
public interface ISample
{
int Id { get; }
}

public interface ISampleTransient : ISample
{
}

public interface ISampleScoped : ISample
{
}

public interface ISampleSingleton : ISample
{
}

public class Sample : ISampleTransient, ISampleScoped, ISampleSingleton
{
private static int _counter;
private int _id;

public Sample()
{
_id = ++_counter;
}

public int Id => _id;
}

Startup.ConfigureServices 中註冊三種不同生命週期的服務。如下:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISampleTransient, Sample>();
services.AddScoped<ISampleScoped, Sample>();
services.AddSingleton<ISampleSingleton, Sample>();
// Singleton 也可以用以下方法註冊
// services.AddSingleton<ISampleSingleton>(new Sample());
}
}

Service Injection

只要是透過 WebHost 產生實例的類別,都可以在建構子定義型態注入

所以 Controller、View、Filter、Middleware 或自訂的 Service 等都可以被注入。
此篇我只用 Controller、View、Service 做為範例。

Controller

在 HomeController 中注入上例的三個 Services:

Controllers\HomeController.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
public class HomeController : Controller
{
private readonly ISample _transient;
private readonly ISample _scoped;
private readonly ISample _singleton;

public HomeController(
ISampleTransient transient,
ISampleScoped scoped,
ISampleSingleton singleton)
{
_transient = transient;
_scoped = scoped;
_singleton = singleton;
}

public IActionResult Index() {
ViewBag.TransientId = _transient.Id;
ViewBag.TransientHashCode = _transient.GetHashCode();

ViewBag.ScopedId = _scoped.Id;
ViewBag.ScopedHashCode = _scoped.GetHashCode();

ViewBag.SingletonId = _singleton.Id;
ViewBag.SingletonHashCode = _singleton.GetHashCode();
return View();
}
}

Views\Home\Index.cshtml

1
2
3
4
5
6
7
<table border="1">
<tr><td colspan="3">Cotroller</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
<tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
<tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>

輸出內容如下:

[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入(Dependency Injection) - Service 生命週期 - Controller
從左到又打開頁面三次,可以發現 Singleton 的 Id 及 HashCode 都是一樣的,此例還看不太出來 TransientScoped 的差異。

Service 實例產生方式:

[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入(Dependency Injection) - 實例產生動畫

圖例說明:

  • ASingleton 物件實例
    一但實例化,就會一直存在於 DI 容器中。
  • BScoped 物件實例
    每次 Request 就會產生新的實例在 DI 容器中,讓同 Request 週期的使用方,拿到同一個實例。
  • CTransient 物件實例
    只要跟 DI 容器請求這個類型,就會取得新的實例。

View

View 注入 Service 的方式,直接在 *.cshtml 使用 @inject

Views\Home\Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@using MyWebsite

@inject ISampleTransient transient
@inject ISampleScoped scoped
@inject ISampleSingleton singleton

<table border="1">
<tr><td colspan="3">Cotroller</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
<tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
<tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>
<hr />
<table border="1">
<tr><td colspan="3">View</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@transient.Id</td><td>@transient.GetHashCode()</td></tr>
<tr><td>Scoped</td><td>@scoped.Id</td><td>@scoped.GetHashCode()</td></tr>
<tr><td>Singleton</td><td>@singleton.Id</td><td>@singleton.GetHashCode()</td></tr>
</table>

輸出內容如下:

[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入(Dependency Injection) - Service 生命週期 - View

從左到又打開頁面三次,Singleton 的 Id 及 HashCode 如前例是一樣的。
TransientScoped 的差異在這次就有明顯差異,Scoped 在同一次 Request 的 Id 及 HashCode 都是一樣的,如紅綠籃框。

Service

簡單建立一個 CustomService,注入上例三個 Service,程式碼類似 HomeController。如下:

Services\CustomService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomService
{
public ISample Transient { get; private set; }
public ISample Scoped { get; private set; }
public ISample Singleton { get; private set; }

public CustomService(ISampleTransient transient,
ISampleScoped scoped,
ISampleSingleton singleton)
{
Transient = transient;
Scoped = scoped;
Singleton = singleton;
}
}

註冊 CustomService

Startup.cs

1
2
3
4
5
6
7
8
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<CustomService, CustomService>();
}
}

第一個泛型也可以是類別,不一定要是介面。
缺點是使用方以 Class 作為相依關係,變成強關聯的依賴。

在 View 注入 CustomService:

Views\Home\Index.cshtml

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
29
30
@using MyWebsite

@inject ISampleTransient transient
@inject ISampleScoped scoped
@inject ISampleSingleton singleton
@inject CustomService customService

<table border="1">
<tr><td colspan="3">Cotroller</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
<tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
<tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>
<hr />
<table border="1">
<tr><td colspan="3">View</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@transient.Id</td><td>@transient.GetHashCode()</td></tr>
<tr><td>Scoped</td><td>@scoped.Id</td><td>@scoped.GetHashCode()</td></tr>
<tr><td>Singleton</td><td>@singleton.Id</td><td>@singleton.GetHashCode()</td></tr>
</table>
<hr />
<table border="1">
<tr><td colspan="3">Custom Service</td></tr>
<tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
<tr><td>Transient</td><td>@customService.Transient.Id</td><td>@customService.Transient.GetHashCode()</td></tr>
<tr><td>Scoped</td><td>@customService.Scoped.Id</td><td>@customService.Scoped.GetHashCode()</td></tr>
<tr><td>Singleton</td><td>@customService.Singleton.Id</td><td>@customService.Singleton.GetHashCode()</td></tr>
</table>

輸出內容如下:

[鐵人賽 Day04] ASP.NET Core 2 系列 - 依賴注入(Dependency Injection) - Service 生命週期 - Servie

從左到又打開頁面三次:

  • Transient
    如預期,每次注入都是不一樣的實例。
  • Scoped
    在同一個 Requset 中,不論是在哪邊被注入,都是同樣的實例。
  • Singleton
    不管 Requset 多少次,都會是同一個實例。

參考

Introduction to Dependency Injection in ASP.NET Core
ASP.NET Core Dependency Injection Deep Dive