ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection)

-- Pageviews

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

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 容器的註冊方式也很簡單,在 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
public interface ISample
{
int Id { get; }
}

public class Sample : ISample
{
private static int _counter;

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

public int Id { get; }
}

要注入的 Service 需要在 ConfigureServices 中註冊實做類別。Startup.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 Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

// 在 Startup 註冊服務
services.AddScoped<ISample, Sample>();
}

public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
"default",
"{controller=Home}/{action=Index}/{id?}"
);
});
}
}
}
  • 第一個泛型為注入的類型
    建議用 Interface 來包裝,這樣在才能把相依關係拆除。
  • 第二個泛型為實做的類別

ASP.NET Core 3 開始,建議透過 Generic Host 建立 Web Host,所以也能用 HostBuilter 中的 ConfigureServices 方法註冊:

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
31
32
33
34
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace MyWebsite
{
public class Program
{
public static void Main(string[] args)
{
var hostBuilder = CreateHostBuilder(args);
var host = hostBuilder.Build();
host.Run();
}

private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// 在 Generic Host Builder 註冊服務
// services.AddScoped<ISample, Sample>();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureServices(services =>
{
// 在 Web Host Builder 註冊服務
// services.AddScoped<ISample, Sample>();
})
.UseStartup<Startup>();
});
}
}

注意!重複註冊,會產生出重複的結果。

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
17
18
19
20
21
22
using Microsoft.AspNetCore.Mvc;

namespace MyWebsite.Controllers
{
public class HomeController : Controller
{
private readonly ISample _sample;

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

public string Index()
{
return $"[{nameof(ISample)}]\r\n"
+ $"Id: {_sample.Id}\r\n"
+ $"HashCode: {_sample.GetHashCode()}\r\n"
+ $"Type: {_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 實例。

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

ASP.NET Core 3 系列 - 依賴注入 (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 中註冊三種不同生命週期的服務。如下:

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());
}
}

如果有特殊需求,也可以透過委派的方式註冊。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISampleTransient>(srv => {
var sample = new Sample();
// Do something ...
return sample;
});
services.AddScoped<ISampleScoped>(srv => new Sample());
services.AddSingleton<ISampleSingleton>(srv => 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
29
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>

輸出內容如下:

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

Service 實例產生方式:

ASP.NET Core 3 系列 - 依賴注入 (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>

輸出內容如下:

ASP.NET Core 3 系列 - 依賴注入 (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:

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

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

Views\Home\Index.cshtml 注入 CustomService:

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>

輸出內容如下:

ASP.NET Core 3 系列 - 依賴注入 (Dependency Injection) - Service 生命週期 - Servie

從左到又打開頁面三次:

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

參考