[聚合文章] 如何在 .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 外,常見我們還會依 職責 拆分 OrderServicePaymentService … 等。

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 直接 newOrderService 內,無法對 PaymentService 做 mock

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

Task

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

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

Architecture

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

Implementation

IPaymentService

IPayment.cs

public interface IPaymentService
{
    string PayCreditCard();
}

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

因為 OrderControllerOrderService 的高階模組,由 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() ,其中也因為要呼叫 PaymentServicePayCreditCard() ,根據 界面隔離原則 ,因此 IPaymentServicePayCreditCard()

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,還要自己建立 ServiceCollectionServiceProvider ,但在 ASP.NET Core,我們有更簡單的方法。

25 行

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

在 ASP.NET Core 要設定 DI 很簡單,不用自己建立 ServiceCollectionServiceProvider ,只要在 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

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