[聚合文章] 如何在 .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() ,其中 Service2 是 Service1 的 dependency。

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

依賴反轉原則

DIP : Dependency Inversion Principle

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

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

所以高階模組 Program 與 Service1 定義出 IService1 與 IService2 interface,由低階模組 Service1 與 Service2 去實現,此時高階模組 Program 與 Service1 不再直接相依於 Service1 與 Service2 ,而是反過來相依於 Program 與 Service1 所定義的 IService1 與 IService2 ,符合 依賴反轉原則 。

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

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

Implementation

IService2

IService2.cs

interface IService2
{
    void SayHello();
}

因為 Service1 為 Service2 的高階模組,由 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();
}

因為 Program 為 Service1 的高階模組,由 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

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