相依性注入(DI)對我來說就是將服務放置到 DI 容器,藉由 DI 容器統一管理服務,就是這麼簡單的東西。

這邊來談談我認為 DI 帶來的好處。

  1. 降低類別彼此間的耦合性,藉由將服務之間的相依改於依賴 DI 容器。
  2. 方便我們撰寫單元測試,對單一服務的測試只需對該服務進行測試,藉由注入測試用的假服務,便可以專注在測試的服務本身。
  3. 註冊服務到 DI 容器時就決定其生命週期,其他服務若需要取得該服務,便不用特別去處理依賴服務的生命週期,交給 DI 即可。
  4. 若以介面方式註冊服務到 DI,便可以輕鬆的替換服務實體。

當然還有很多能夠講的優點就不一一列舉,本文的重點還是放在使用上面。

生命週期

依照官方的說明 .NET Core DI 有三種生命週期如下。

  1. 暫時性(Transient) - 每次從服務容器要求暫時性存留期服務時都會建立它們。此存留期最適合用於輕量型的無狀態服務。
  2. 具範圍(Scoped) - 具範圍存留期服務會在每次用戶端要求(連線)時建立一次。
  3. 單一(Singleton) - 當第一次收到有關單一資料庫存留期服務的要求時(或是當執行 Startup.ConfigureServices 且隨著服務註冊指定執行個體時),即會建立單一資料庫存留期服務。每個後續要求都會使用相同的執行個體。若應用程式要求單一資料庫行為,建議您允許服務容器管理服務的存留期。不要實作單一資料庫設計模式並為使用者提供可用來管理類別中物件存留期的程式碼。

事前準備

建立一個 .NET Core API 起始專案,加入介面與實作該介面的類別,以及預期使用該服務的控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ITestService.cs
public interface ITestService
{
public int Add(int a, int b);
}

// TestService.cs
public class TestService : ITestService
{
public int Add(int a, int b) => a + b;
}

// HomeController.cs
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase {}

使用方式

以下所有範例的註冊內容都撰寫在Startup.cs裡的ConfigureServices(IServiceCollection services)方法中,也會在範例程式首行標註檔案名稱。

服務註冊方法

雖然官方提供了幾個方法,但我偏愛以下的註冊方式,透過介面來進行註冊,並提供實作。

1
2
3
4
// Startup.cs
services.AddTransient<ITestService, TestService>();
services.AddScoped<ITestService, TestService>();
services.AddSingleton<ITestService, TestService>();

簡單就可以把服務註冊到 DI 容器,並透過建構式注入方式取得。

1
2
3
4
5
6
7
8
9
10
11
12
13
// HomeController.cs
public class HomeController : ControllerBase
{
private readonly ITestService _testService;

public HomeController(ITestService testService)
{
_testService = testService;
}

[HttpGet("Add/{a}/{b}")]
public int AddTwoNumber(int a, int b) => _testService.Add(a, b);
}

這樣就簡單的完成 DI 注入並取得對應實體。

組態設定(Configuration)

這邊將介紹如何透過 IOption<T> 以強型別的方式來取得組態值。

在組態檔增加以下內容。

1
2
3
4
// appsettings.json
"TestConfig": {
"Value": "Test Config Value"
}

建立與組態格式一樣的型別,類別名稱不必一樣,這邊只是為了方便取一樣的名稱,屬性名稱一樣即可。

1
2
3
4
5
// TestConfig.cs
public class TestConfig
{
public string Value { get; set; }
}

完成後就可以來註冊組態了。

1
2
// Startup.cs
services.Configure<TestConfig>(Configuration.GetSection("TestConfig"));

這邊具體發生什麼事並非本文重點,對於取得組態有興趣的可以參考Configuration.GetSection(String) 方法,如果對services.Configure底層實作有興趣的可以看 Source Code OptionsServiceCollectionExtensions.cs

然後修改建構式,以及新增方法來取得我們的 TestConfig 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// HomeController.cs
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
private readonly ITestService _testService;
private readonly TestConfig _testConfig;

public HomeController(ITestService testService, IOptions<TestConfig> options)
{
_testService = testService;
_testConfig = options.Value;
}

[HttpGet("add/{a}/{b}")]
public int AddTwoNumber(int a, int b) => _testService.Add(a, b);

[HttpGet("testconfig")]
public string GetTestConfigValue() => _testConfig.Value;
}

擴充方法

如果服務本身有使用到組態檔時,可以考慮將其抽成擴充方法,讓擴充方法與對應的組態內容包成一組,在缺少對應的組態內容時拋出錯誤。

建立需使用組態內容的服務與擴充方法。

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
// TestHelper.cs
public class TestHelper
{
private readonly TestConfig _testConfig;

public TestHelper(IOptions<TestConfig> options)
{
_testConfig = options.Value;
}

public string DoSomthingAndGetValue() => _testConfig.Value;
}

// ServiceCollectionExtension.cs
public static class ServiceCollectionExtension
{
public static IServiceCollection AddTestHelper(this IServiceCollection serviceCollection, Action<TestConfig> options)
{
serviceCollection.AddScoped<TestHelper>();

if (options == null)
{
throw new ArgumentNullException(nameof(options), @"Please provide options for TestHelper.");
}

serviceCollection.Configure(options);
return serviceCollection;
}
}

完成後就可以來註冊擴充方法了。

1
2
// Startup.cs
services.AddTestHelper(options => Configuration.GetSection("TestConfig").Bind(options));

使用方式就與一般註冊一樣,注入 TestHelper 到建構式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HomeController.cs
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
private readonly ITestService _testService;
private readonly TestHelper _testHelper;

public HomeController(ITestService testService, TestHelper testHelper)
{
_testService = testService;
_testHelper = testHelper;
}

[HttpGet("add/{a}/{b}")]
public int AddTwoNumber(int a, int b) => _testService.Add(a, b);

[HttpGet("testconfig")]
public string GetTestConfigValue() => _testHelper.DoSomthingAndGetValue();
}

總結

這篇是我在學習 DI 過程中的心得與經驗分享,也分享了幾個我認為在應用 DI 時的最佳實作,當然還有很多內容可以寫,但有些比較零碎的內容因為篇幅關係就不納入。

相關的程式碼我也傳到我的 Github 上,若對於文章的範例程式碼不清楚可以上去看每一個 Commit 修改的內容。

參考資料
.NET Core 中的相依性插入 | Microsoft Docs
ASP.NET Core 中的選項模式 | Microsoft Docs
範例程式碼