透過 .NET Core 內建的依賴注入套件,除了可以讓我們輕鬆實現 Dependency Injection 依賴注入的設計模式,更可以幫助我們把程式碼寫得更職責分離,在 ASP.NET Core 的專案範架構中,已經被列為基礎架構,幾乎所有應用服務都是基於 DI 來設計,如果我們想要在自己的應用程式中加入這好用的工具,可以參考這篇作法,如何將內建的依賴注入工具整合至主控台應用程式中。

.NET Core 提供一套 Microsoft.Extensions.DependencyInjection 依賴注入套件,透過這個套件,我們可以輕鬆出搭建具 DI 功能的架構,並且擁有高效能的註冊、解析依賴物件的能力。

有關 DI 套件的效能比較,可以參考 Daniel Palme 的這篇 IoC Container Benchmark - Performance comparison 文章所提供的數據。

這裡最終希望搭建出如下圖的架構,在高階模組 Host Program 之上,建立 DI Container 作為依賴注入物件的容器,並由 App 作為整個應用程式的起始點,將所相依的服務注入至 App 內使用,所有的功能都透過 DI 容器來幫我處理建立、注入的作業。

!基本架構

本篇完整範例程式碼請參考 poychang/Demo-DI-Console-App

建立專案

首先,使用 Visual Studio 預設的主控台應用程式(.NET Core)專案範本來建立一個空白的專案。

建立主控台應用程式專案

預設的主控台應用程式專案裡面很簡單,只有一個 Program.cs 檔案。

安裝套件

你可以透過指令的方式或 Visual Studio 的 NuGet 套件管理器,安裝微軟官方提供的依賴注入套件 Microsoft.Extensions.DependencyInjection,或者透過指令安裝 Install-Package Microsoft.Extensions.DependencyInjection

這個套件就是 DI 框架,我們可以用他來建立 DI 容器,並透過他來註冊、解析依賴物件。

如果你想看看這個 DI 框架是怎麼被實做出來的,可以直接到官方在 GitHub 上的 Repo 查看他的原始碼。

撰寫程式架構

關注點分離能夠讓我們寫程式時,專注在特定的執行邏輯上,因此我們將業務邏輯程式主要用於執行應用程式的邏輯分開,這裡建立一個 App.cs 作為主要執行應用程式邏輯的類別。

public class App
{
    private readonly IService _service;

    public App(IService service)
    {
        _service = service;
    }

    public void Run()
    {
        _service.SayHello();
    }
}

在這個主要邏輯中,注入了一種具有 SayHello 方法的服務,我們希望這個服務可以切換成說中文或英文的。

因此我們建立了兩個業務邏輯程式叫做 ChtService.csEngService.cs,並且都實作了 IService 介面,這個介面定義了一個 SayHello 方法,就只會印出中文或英文的 Hello World。

程式碼很簡單,如下:

public interface IService
{
    string SayHello();
}

public class ChtService : IService
{
    public void SayHello()
    {
        Console.WriteLine("哈囉世界!");
    }
}

public class EngService : IService
{
    public void SayHello()
    {
        Console.WriteLine("Hello World!");
    }
}

這兩個功能在之後的使用上,可以在不修改主要程式邏輯(App.cs),只需要在註冊 DI 依賴物件的地方,透過抽換的方式來決定要使用哪一種服務。

接著我們在高階模組 Program.cs 上,主要做三件事:

  1. 建立容器
  2. 註冊依賴物件
  3. 執行主要程式邏輯
public static void Main(string[] args)
{
    // 1. 建立依賴注入的容器
    var serviceCollection = new ServiceCollection();
    // 2. 註冊服務
    serviceCollection.AddTransient<App>();
    serviceCollection.AddTransient<IService, ChtService>();
    // 建立依賴服務提供者
    var serviceProvider = serviceCollection.BuildServiceProvider();

    // 3. 執行主服務
    serviceProvider.GetRequiredService<App>().Run();
}

如此一來,這個主控台應用程式在執行時,就會先將 DI 容器建立好,也將整個應用程式會使用到的依賴物件也一併準備好,而且在建立的過程中,會根據各物件依賴的服務,藉由建構式注入的方式,將物件自動組合完成,像是一種 High Order Object 的感覺。

這樣具有 DI 架構的主控台應用程式就完成了。

註冊服務

這裡有一個關鍵是註冊服務這件事,註冊的同時會賦予服務生命週期,ServiceCollection 這個服務容器,會負責將所註冊的 Interface 與 Class 做對應,並依據生命週期來決定何時建立及注入,然而物件在使用注入的服務時,有時候會希望注入的是全應用程式共用的物件,或者總是全新的物件,這時我們就可以在註冊的時候,明確的指定依賴的服務生命週期為何。

Microsoft.Extensions.DependencyInjection 提供以下三種生命週期的註冊方式:

  • AddSingleton() : 程式啟動後,會創建一個新的實體,也就是執行時期只有一個實體,基本上生命週期與應用程式一致
  • AddTransient() : 每次注入時,都會重新創建一個新的實體,例如在不同的建構式中注入,都會取得新的實體
  • AddScoped() : 在每個 Scope 中都會重新創建一個新的實體,用 ASP.NET Core 網站的角度來看,一個 Http Request 就是一個 Scope 級別,因此每次 Http Request 都會取得一個新的實體

本篇完整範例程式碼請參考 poychang/Demo-DI-Console-App

後記

一般來說,在建立有 DI 架構的應用程式時,會建議 DI 容器的建立時機要盡可能早,最好應用程式一啟動就執行,讓 DI 容器去管理物件的生命週期,我們只需要專注在商業邏輯如何運用就好。


參考資料:


Poy Chang

Trial and Error