[聚合文章] 如何在 .NET Core 的 Console App 使用 DI ?

.Net 2017-11-29 18 阅读

Visual Studio 在 ASP.NET Core Web Application 的 template 中,預設已經可使用 DI,若要在 console app 也使用 DI,需要另外設定。

Version

Visual Studio 2017 15.4.5

.NET Core 2.0

Microsoft.Extensions.DependencyInjection 2.0.0

安裝 NuGet 套件

GUI 安裝

  1. 在 project 選擇 Dependencies
  2. 按滑鼠右鍵選擇 Manage NuGet Packages...

DI 主要是在 Microsoft.Extensions.DependencyInjection 套件裡,console app 預設沒有安裝,需要手動自行安裝。

  1. 選擇 Browse
  2. 輸入 DependencyInjection
  3. 選擇 Microsoft.Extensions.DependencyInjection 套件
  4. Install 安裝套件

安裝完成後,在 Dependencies 下的 NuGet 會看到 Microsoft.Extensions.DependencyInjection 套件。

指令安裝

Tools -> NuGet Package Manager -> Package Manager Console

啟動 package manager console

PM> Install-Package Microsoft.Extensions.DependencyInjection

使用 Install-Package 指令安裝 Microsoft.Extensions.DependencyInjection 套件。

安裝完成後,在 Dependencies 下的 NuGet 會看到 Microsoft.Extensions.DependencyInjection 套件。

Architecture

Service1 使用 delagation 手法,將 Execute() 轉發給 Service2.SayHello() ,其中 Service2Service1 的 dependency。

傳統寫法我們會在 Service1 內去 new Service2 ,也就是高階模組 Service1 直接相依於低階模組 Service2 ,這就違反了 依賴反轉原則

依賴反轉原則

DIP : Dependency Inversion Principle

高階模組不應該相依於低階模組;低階模組應該相依於高階模組所定義的抽象

白話 : 原本是高層依賴於底層,現在是反過來底層依賴於高層所定義的 interface

所以高階模組 ProgramService1 定義出 IService1IService2 interface,由低階模組 Service1Service2 去實現,此時高階模組 ProgramService1 不再直接相依於 Service1Service2 ,而是反過來相依於 ProgramService1 所定義的 IService1IService2 ,符合 依賴反轉原則

因此我們改用 依賴注入 方式,由 Service1 的 constructor 將符合 IService2 interface 的 Service2 注入進去。

而 DI Container 就是負責幫我們 new 物件,然後由 constructor 注入。

Implementation

IService2

IService2.cs

interface IService2
{
    void SayHello();
}

因為 Service1Service2 的高階模組,由 Service1 定義出 IService2 ,由 Service2 所依賴。

Service2

Service2.cs

class Service2 : IService2
{
    public void SayHello()
    {
        Console.WriteLine("Hello World");
    }
}

低階模組 Service2 實現 (依賴) IService2

IService1

IService1.cs

interface IService1
{
    void Execute();
}

因為 ProgramService1 的高階模組,由 Program 定義出 IService1 ,由 Service1 所依賴。

Service1

Service1.cs

class Service1 : IService1
{
    private readonly IService2 service2;

    public Service1(IService2 service2)
    {
        this.service2 = service2;
    }

    public void Execute()
    {
        this.service2.SayHello();
    }
}

低階模組 Service1 實現 (依賴) IService1

第 3 行

private readonly IService2 service2;

public Service1(IService2 service2)
{
    this.service2 = service2;
}

傳統我們都會在 Service1 的 constructor 去 new Service2

private readonly IService2 service2;

public Service1()
{
    this.service2 = new Service2();
}

這樣 Service1 就直接耦合了 Service2 ,現在我們改用在 constructor 依賴注入 IService2 型別的 Service2

這樣做有幾個優點 :

  • 由原本相依於 Service2 ,改相依於 IService2 interface,符合 依賴反轉原則
  • 原本直接在 constructor 去 new ,高階模組無法抽換低階模組,現在高階模組可透過 constructor 直接換掉低階模組,原來的 Service1 程式碼不用變動,符合 開放封閉原則
  • 單元測試時,可直接透過 constructor 注入 mock 物件隔離測試

OOP 心法

若使用 DI 寫法,則不須再使用 new ,讓程式碼更為乾淨,類似 static class 只要一行程式碼就可解決

Program

Program.cs

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddTransient<IService1, Service1>();
        services.AddTransient<IService2, Service2>();

        var serviceProvider = services.BuildServiceProvider();
        IService1 service1 = serviceProvider.GetService<IService1>();
        service1.Execute();
    }
}

Service1 已經用了 依賴注入 ,讓我們可以在 constructor 注入相依的 Service2 ,傳統我們會這樣建立 Service1 :

class Program
{
    static void Main(string[] args)
    {
        var service1 = new Service1(new Service2());
        service1.Execute();
    }
}

直接使用 new ,並在其 constructor 傳進 new Service2()

但實務上 service 與 service 間的關係會更複雜,因此 constructor 寫法不會這麼簡單。

而 DI container 就是幫我們不用在 constructor 寫複雜的 new

第 5 行

var services = new ServiceCollection();
services.AddTransient<IService1, Service1>();
services.AddTransient<IService2, Service2>();

要讓 DI container 幫我們 new ,首先必須告訴 DI container :

當遇到 xxx interface 時,請幫我注入 yyy class

.NET 提供了 ServiceCollection ,由它負責蒐集所有 interface 與 class 的 mapping。

  • AddTrasient() : 每次注入時,都重新 new 一個新的 instance
  • AddScoped() : 每個 request 都重新 new 一個新的instance
  • AddSingleton() : 程式啟動後會 new 一個 instance,之後會重複使用,也就是運行期間只有一個 instance

泛型參數部份 :

  • 第一個參數為 interface
  • 第二個參數為 class

也就是告訴 ServiceCollection ,當 constructor 的型別為此 interface 時,請幫我 new 這個 class。

public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services) 
    where TService : class
    where TImplementation : class, TService;

其中 TImplementation 的 constraint 為 TService ,也就是若你這樣寫

services.AddTransient<IService1, Service2>();

因為 Service2 根本沒有實作 IService1 ,編譯會錯誤。

OOP 心法

在使用泛型時,一定要搭配 constraint,才會發揮泛型的威力

第 9 行

var serviceProvider = services.BuildServiceProvider();
IService1 service1 = serviceProvider.GetService<IService1>();

DI container 要靠 service provider 來建立 object,所以下一步要靠 ServiceCollection.BuildServiceProvider() 來產生 ServiceProvider

之後就可用 ServiceProvider.GetService() 來建立 object,而不須使用 new Service1(new Service2())

GetService() 泛型參數要傳的是 interface,而不是 class,因為 constructor 宣告的是 interface 型別,不是 class 型別。

Q : 使用 service provider 建立 object,與自己 new object,差別在哪裡 ?

A : 你只須用 service provider 建立最外層的 object 即可,剩下相依物件,DI container 會幫你建立;但若你使用 new ,怎每一層的相依物件都要自己處理。

11 行

service1.Execute();

Object 已經被 DI container 建立,就可以使用一般的方式使用 method。

Q : 依賴注入與 DI container 除了讓我們不用複雜的 new ,還有其他優點嗎 ?

class Program
{
    static void Main(string[] args)
    {
        var service1 = new Service1();
        service1.Execute();
    }
}

在沒有使用 依賴注入 與 DI container 之前, Program 只能去 new Service1() ,至於 Service1 用了哪些相依物件, Program 高階模組無法決定,也就是高階模組已經被低階模組綁死了,無從決定低階模組到底用了什麼相依物件。

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddTransient<IService1, Service1>();
        services.AddTransient<IService2, Service2>();

        var serviceProvider = services.BuildServiceProvider();
        IService1 service1 = serviceProvider.GetService<IService1>();
        service1.Execute();
    }
}

第 5 行

var services = new ServiceCollection();
services.AddTransient<IService1, Service1>();
services.AddTransient<IService2, Service2>();

高階模組 Program 可以決定低階模組 Service1 相依什麼物件,只要在 ServiceCollection 加以定義即可。

也就是高階模組不再相依於低階模組,而是高階模組可以自行定義低階模組的相依物件,符合 依賴反轉原則

Q : 高階模組能決定低階模組有什麼意義呢 ?

一旦高階模組可以決定低階模組,就可以在程式一開始 if else 就決定使用那些低階模組,決定之後,程式就可以很簡單的執行,不須再 if else 判斷。

但若高階模組無法決定低階模組,則在低階模組勢必增加很多 if else 判斷,判斷在什麼條件下,使用什麼低階模組,而且 if else 會分散在各低階模組,程式複雜度會提高很多。

Conclusion

  • Console app 無法使用 DI container,須自行安裝 Microsoft.Extensions.DependencyInjection ,可使用 GUI 安裝,可以使用指令安裝
  • DI 可以讓程式碼更乾淨,用起來類似 static 只要一行
  • DI container 讓 依賴注入 更容易實現,也符合 依賴反轉原則

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

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。