[聚合文章] 如何在 .NET Core Web API 使用 DI ?

.Net 2017-12-03 18 阅读

.NET Core 已經內建 DI,讓我們可以享受 DI container 實現 依賴注入 的方便,Visual Studio 2017 在 ASP.NET Core Web Application 的 template 中,預設已經可直接使用 DI,不需另外設定。

Version

Visual Studio 2017 15.4.5

.NET Core 2.0

Microsoft.Extensions.DependencyInjection 2.0.0

User Story

一個常見的分層架構,除了 OrderController 外,常見我們還會依 職責 拆分 OrderService 與 PaymentService … 等。

OrderController.Index() 呼叫 OrderService.CreateOrder() ,然後 OrderService.CreateOrder() 再去呼叫 PaymentService.PayCreditCard() 。

傳統我們會這樣寫 code :

OrderController.cs

class OrderController
{
  private readonly OrderService orderService;
  
  public OrderController()
  {
      this.orderService = new OrderService();
  }
  
  public Index()
  {
      this.orderService.CreateOrder();
  }
}

OrderService.cs

class OrderService
{
    private readonly PaymentService paymentService;
  
    public OrderService()
    {
        this.paymentService = new PaymentService();      
    }
  
    public void CreateOrder()
    {
        this.paymentService.payCreditCard();      
    }
}

PaymentService.cs

class PaymentService
{
    public PayCreditCard()
    {
        ...
    }
}

這是傳統 OOP 寫法,但這有幾個問題 :

  • PaymentService 直接被 OrderService new 在裡面,因此 OrderController 只要用了 OrderService 就必須得用 PaymentService ,完全沒有改變 PaymentService 的空間,也就是 OrderController 直接耦合 PaymentService ,無法替換 PaymentService
  • 無法對 OrderService 做單元測試,因為 PaymentService 直接 new 在 OrderService 內,無法對 PaymentService 做 mock

這就是典型的 耦合 ,也就是 高階模組 直接相依於低階模組,然後高階模組就被低階模組綁死了,無法抽換,也無法單元測試。

Task

目前 OrderController 與 PaymentService 直接偶合在一起,也就是用戶端只要用了 OrderController ,就一定得使用 PaymentService ,別無選擇。

現在用戶端希望使用 OrderController 時,還能決定 OrderController 的相依物件,不見的一定要 PaymentService ,完全由用戶端決定,也就是對用戶端與 PaymentService 解耦合。

Architecture

OrderController 、 OrderService 與 PaymentService 已經符合 依賴反轉原則 ,也使用了 依賴注入 ,但要如何設定 依賴注入容器 呢 ?

Implementation

IPaymentService

IPayment.cs

public interface IPaymentService
{
    string PayCreditCard();
}

因為 OrderService 為 PaymentService 的高階模組,由 OrderService 定義出 IPaymentService ,由 PaymentService 所依賴。

PaymentService

PaymentService.cs

public class PaymentService : IPaymentService
{
    public string PayCreditCard()
    {
        return "PaymentService.PayCreditCard()";
    }
}

低階模組 PaymentService 實現 (依賴) IPaymentService 。

IOrderService

IOrderService.cs

public interface IOrderService
{
    string CreateOrder();
}

因為 OrderController 為 OrderService 的高階模組,由 OrderController 定義出 IOrderService ,由 OrderService 所依賴。

OrderService

OrderService.cs

public class OrderService : IOrderService
{
    private readonly IPaymentService paymentService;

    public OrderService(IPaymentService paymentService)
    {
        this.paymentService = paymentService;
    }

    public string CreateOrder()
    {
        return this.paymentService.PayCreditCard();
    }
}

低階模組 OrderService 實現 (依賴) IOrderService 。

第 3 行

private readonly IPaymentService paymentService;

我們宣告了 IPaymentService 型別的 field,注意此為抽象型別 interface,而非具體型別,所以 OrderService 不會直接依賴 PaymentService ,而是依賴 IPaymentService ,符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象 。

第 5 行

public OrderService(IPaymentService paymentService)
{
    this.paymentService = paymentService;
}

PaymentService 由 constructor 的參數傳進來,而不是直接 new 在 constructor 內。

如此 OrderService 的相依物件,就可透過 constructor 傳進來,從此高階模組 OrderService 就不再直接相依於低階模組 PaymentService ,而是由更高階模組 OrderController 決定 OrderService 該依賴什麼物件,再由 constructor 參數傳進來,這就是 依賴注入 。

注意 constructor 參數的型別是 IPaymentService ,而不是 PaymentService ,如此才會符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象 。

我們改用 依賴注入 後有幾個優點 :

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

OOP 心法

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

10 行

public string CreateOrder()
{
    return this.paymentService.PayCreditCard();
}

由 CreateOrder() 去呼叫 PaymentService.PayCreditCard() ,其中也因為要呼叫 PaymentService 的 PayCreditCard() ,根據 界面隔離原則 ,因此 IPaymentService 有 PayCreditCard() 。

OrderController

OrderController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NETCoreAPIDI.Services;

namespace NETCoreAPIDI.Controllers
{
    [Route("api/[controller]")]
    public class OrderController : Controller
    {
        private readonly IOrderService orderService;

        public OrderController(IOrderService orderService)
        {
            this.orderService = orderService;
        }

        // GET api/order
        [HttpGet]
        public string Get()
        {
            return this.orderService.CreateOrder();
        }
    }
}

13 行

private readonly IOrderService orderService;

我們宣告了 IOrderService 型別的 field,注意此為抽象型別 interface,而非具體型別,所以 OrderController 不會直接依賴 OrderService ,而是依賴 IOrderService ,符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象 。

15 行

public OrderController(IOrderService orderService)
{
    this.orderService = orderService;
}

OrderService 由 constructor 的參數傳進來,而不是直接 new 在 constructor 內。

如此 OrderController 的相依物件,就可透過 constructor 傳進來,從此高階模組 OrderController 就不再直接相依於低階模組 OrderService ,而是由更高階模組決定 OrderController 該依賴什麼物件,再由 constructor 參數傳進來,這就是 依賴注入 。

注意 constructor 參數的型別是 IOrderService ,而不是 OrderService ,如此才會符合 依賴反轉原則 : 高階模組不直接依賴低階模組,而是依賴抽象 。

20 行

// GET api/order
[HttpGet]
public string Get()
{
    return this.orderService.CreateOrder();
}

當 GET 時,執行 OrderService.CreateOrder() 。

Startup

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NETCoreAPIDI.Services;

namespace NETCoreAPIDI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddTransient<IOrderService, OrderService>();
            services.AddTransient<IPaymentService, PaymentService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
        }
    }
}

在 console app 時,若我們要使用 DI,還要自己建立 ServiceCollection 與 ServiceProvider ,但在 ASP.NET Core,我們有更簡單的方法。

25 行

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddTransient<IOrderService, OrderService>();
    services.AddTransient<IPaymentService, PaymentService>();
}

在 ASP.NET Core 要設定 DI 很簡單,不用自己建立 ServiceCollection 與 ServiceProvider ,只要在 ConfigureServices() 下使用 AddTrasient() 建立 interface 與 class 的 mapping 即可。

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

泛型參數部份 :

  • 若只有 class,沒有 interface,如 OrderController :
    • 只須傳入第一個參數為 class
  • 若有 interface 與對應 class
    • 第一個參數為 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<IPaymentService, OrderService>();

因為 OrderService 根本沒有實作 IPaymentService ,編譯會錯誤。

OOP 心法

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

Conclusion

  • ASP.NET Core 已經將 DI container 準備好,可直接使用
  • DI 可以讓程式碼更乾淨,用起來類似 static 只要一行
  • DI container 讓 依賴注入 更容易實現,尤其在用戶端可以完全掌握 低階模組 的相依物件,完全符合 依賴反轉原則

Sample Code

完整的範例可以在我的 GitHub 找到。

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

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