1. 程式人生 > >ASP.NET Core 2 學習筆記(四)依賴註入

ASP.NET Core 2 學習筆記(四)依賴註入

pub framework 三次 DDM order 包裝 差異 限制 cto

原文:ASP.NET Core 2 學習筆記(四)依賴註入

ASP.NET Core使用了大量的依賴註入(Dependency Injection, DI),把控制反轉(Inversion Of Control, IoC)運用的相當巧妙。DI可算是ASP.NET Core最精華的一部分,有用過Autofac或類似的DI Framework對此應該不陌生。
本篇將介紹ASP.NET Core的依賴註入(Dependency Injection)。

DI 容器介紹

在沒有使用DI Framework 的情況下,假設在UserController 要調用UserLogic,會直接在UserController 實例化UserLogic,如下:

public class UserLogic {
    public void Create(User user) {
        // ...
    }
}

public class UserController : Controller {
    public void Register(User user){
        var logic = new UserLogic();
        logic.Create(user);
        // ...
    }
}

以上程序基本沒什麽問題,但是依賴關系差了點。UserController 必須要依賴UserLogic才可以運行,拆出接口改成:

public interface IUserLogic {
    void Create(User user);
}

public class UserLogic : IUserLogic {
    public void Create(User user) {
        // ...
    }
}

public class UserController : Controller {
    private readonly IUserLogic _userLogic;

    public UserController() {
        _userLogic = new UserLogic();
    }

    public void Register(User user){
        _userLogic.Create(user);
        // ...
    }
}

UserController 與UserLogic 的依賴關系只是從Action 移到構造方法中,依然還是很強的依賴關系。

ASP.NET Core透過DI容器,切斷這些依賴關系,實例的產生不在使用方(指上例UserController構造方法中的new),而是在DI容器。
DI容器的註冊方式也很簡單,在Startup.ConfigureServices註冊。如下:

Startup.cs

// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
}

services就是一個DI容器。
此例把MVC的服務註冊到DI容器,等到需要用到MVC服務時,才從DI容器取得對象實例。 

基本上要註入到Service的類沒什麽限制,除了靜態類
以下示例就只是一般的Class繼承Interface:

public interface ISample
{
    int Id { get; }
}

public class Sample : ISample
{
    private static int _counter;
    private int _id;

    public Sample()
    {
        _id = ++_counter;
    }

    public int Id => _id;
}

要註入的Service需要在Startup.ConfigureServices中註冊實例類型。如下:

Startup.cs

// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddScoped<ISample, Sample>();
    }
}
  • 第一個泛型為註入的類型
    建議用Interface來包裝,這樣在才能把依賴關系拆除。
  • 第二個泛型為實例的類型

DI 運行方式

ASP.NET Core的DI是采用Constructor Injection,也就是說會把實例化的物件從建構子傳入。
如果要取用DI容器內的物件,只要在建構子加入相對的Interface即可。例如:

Controllers\HomeController.cs

using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;

namespace MyWebsite.Controllers
{
    public class HomeController : Controller
    {
        private readonly ISample _sample;

        public HomeController(ISample sample)
        {
            _sample = sample;
        }

        public string Index()
        {
            return $"[ISample]\r\n"
                 + $"Id: {_sample.Id}\r\n"
                 + $"HashCode: {_sample.GetHashCode()}\r\n"
                 + $"Tpye: {_sample.GetType()}";
        }
    }
}

輸出內容如下:

[ISample]
Id: 1
HashCode: 52582687
Tpye: MyWebsite.Models.Sample

ASP.NET Core 實例化Controller 時,發現構造方法中有ISample 這個類型的參數,就把Sample 的實例註入給該Controller。

每個Request 都會把Controller 實例化,所以DI 容器會從構造方法註入ISample 的實例,把sample 存到變量_sample 中,就能確保Action 能夠使用到被註入進來的ISample 實例。

註入實例過程,情境如下:

技術分享圖片

Service 生命周期

註冊在DI 容器的Service 分三種生命周期:

  • Transient
    每次註入時,都重新new一個新的實例。
  • Scoped
    每個Request都重新new一個新的實例,同一個Request不管經過多少個Pipeline都是用同一個實例。上例所使用的就是Scoped。
  • Singleton
    被實例化後就不會消失,程式運行期間只會有一個實例。

小改一下Sample 類的代碼:

namespace MyWebsite.Models
{
    public interface ISample
    {
        int Id { get; }
    }

    public interface ISampleTransient : ISample
    {
    }

    public interface ISampleScoped : ISample
    {
    }

    public interface ISampleSingleton : ISample
    {
    }

    public class Sample : ISampleTransient, ISampleScoped, ISampleSingleton
    {
        private static int _counter;
        private int _id;

        public Sample()
        {
            _id = ++_counter;
        }

        public int Id => _id;
    }
}

Startup.ConfigureServices中註冊三種不同生命周期的服務。如下:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<ISampleTransient, Sample>();
        services.AddScoped<ISampleScoped, Sample>();
        services.AddSingleton<ISampleSingleton, Sample>();
        // Singleton 也可以用以下方法註冊
        // services.AddSingleton<ISampleSingleton>(new Sample());
    }
}

Service Injection

只要是透過WebHost產生實例的類型,都可以在構造方法註入。

所以Controller、View、Filter、Middleware或自定義的Service等都可以被註入。
此篇我只用Controller、View、Service做為範例。

Controller

在TestController 中註入上例的三個Services:

Controllers\TestController.cs

using Microsoft.AspNetCore.Mvc;
using MyWebsite.Models;

namespace MyWebsite.Controllers
{
    public class TestController : Controller
    {
        private readonly ISample _transient;
        private readonly ISample _scoped;
        private readonly ISample _singleton;

        public TestController(
            ISampleTransient transient,
            ISampleScoped scoped,
            ISampleSingleton singleton)
        {
            _transient = transient;
            _scoped = scoped;
            _singleton = singleton;
        }

        public IActionResult Index()
        {
            ViewBag.TransientId = _transient.Id;
            ViewBag.TransientHashCode = _transient.GetHashCode();

            ViewBag.ScopedId = _scoped.Id;
            ViewBag.ScopedHashCode = _scoped.GetHashCode();

            ViewBag.SingletonId = _singleton.Id;
            ViewBag.SingletonHashCode = _singleton.GetHashCode();
            return View();
        }
    }
}

Views\Test\Index.cshtml

<table border="1">
    <tr><td colspan="3">Cotroller</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
    <tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
    <tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>

輸出內容如下:

技術分享圖片

從左到又打開頁面三次,可以發現Singleton的Id及HashCode都是一樣的,現在還看不太能看出來Transient及Scoped的差異。

Service 實例產生方式:

技術分享圖片

圖例說明:

  • A為Singleton對象實例
    一但實例化,就會一直存在於DI容器中。
  • B為Scoped對象實例
    每次Request就會產生新的實例在DI容器中,讓同Request周期的使用方,拿到同一個實例。
  • CTransient對象實例
    只要跟DI容器請求這個類型,就會取得新的實例。

View

View註入Service的方式,直接在*.cshtml使用@inject

Views\Test\Index.cshtml

@using MyWebsite.Models

@inject ISampleTransient transient
@inject ISampleScoped scoped
@inject ISampleSingleton singleton

<table border="1">
    <tr><td colspan="3">Cotroller</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
    <tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
    <tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>
<hr />
<table border="1">
    <tr><td colspan="3">View</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@transient.Id</td><td>@transient.GetHashCode()</td></tr>
    <tr><td>Scoped</td><td>@scoped.Id</td><td>@scoped.GetHashCode()</td></tr>
    <tr><td>Singleton</td><td>@singleton.Id</td><td>@singleton.GetHashCode()</td></tr>
</table>

輸出內容如下:

技術分享圖片

從左到又打開頁面三次,Singleton的Id及HashCode如前例是一樣的。
Transient及Scoped的差異在這次就有明顯差異,Scoped在同一次Request的Id及HashCode都是一樣的,如紅紫籃框。

Service

簡單建立一個CustomService,註入上例三個Service,代碼類似TestController。如下:

Services\CustomService.cs

using MyWebsite.Models;

namespace MyWebsite.Services
{
    public class CustomService
    {
        public ISample Transient { get; private set; }
        public ISample Scoped { get; private set; }
        public ISample Singleton { get; private set; }

        public CustomService(ISampleTransient transient,
            ISampleScoped scoped,
            ISampleSingleton singleton)
        {
            Transient = transient;
            Scoped = scoped;
            Singleton = singleton;
        }
    }
}

註冊CustomService

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddScoped<CustomService, CustomService>();
    }
}

第一個泛型也可以是類,不一定要是接口。
缺點是使用方以Class作為依賴關系,變成強關聯的依賴。

在View 註入CustomService:

Views\Home\Index.cshtml

@using MyWebsite
@using MyWebsite.Models
@using MyWebsite.Services

@inject ISampleTransient transient
@inject ISampleScoped scoped
@inject ISampleSingleton singleton
@inject CustomService customService

<table border="1">
    <tr><td colspan="3">Cotroller</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@ViewBag.TransientId</td><td>@ViewBag.TransientHashCode</td></tr>
    <tr><td>Scoped</td><td>@ViewBag.ScopedId</td><td>@ViewBag.ScopedHashCode</td></tr>
    <tr><td>Singleton</td><td>@ViewBag.SingletonId</td><td>@ViewBag.SingletonHashCode</td></tr>
</table>
<hr />
<table border="1">
    <tr><td colspan="3">View</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@transient.Id</td><td>@transient.GetHashCode()</td></tr>
    <tr><td>Scoped</td><td>@scoped.Id</td><td>@scoped.GetHashCode()</td></tr>
    <tr><td>Singleton</td><td>@singleton.Id</td><td>@singleton.GetHashCode()</td></tr>
</table>
<hr />
<table border="1">
    <tr><td colspan="3">Custom Service</td></tr>
    <tr><td>Lifetimes</td><td>Id</td><td>Hash Code</td></tr>
    <tr><td>Transient</td><td>@customService.Transient.Id</td><td>@customService.Transient.GetHashCode()</td></tr>
    <tr><td>Scoped</td><td>@customService.Scoped.Id</td><td>@customService.Scoped.GetHashCode()</td></tr>
    <tr><td>Singleton</td><td>@customService.Singleton.Id</td><td>@customService.Singleton.GetHashCode()</td></tr>
</table>

輸出內容如下:

技術分享圖片

從左到又打開頁面三次:

  • Transient
    如預期,每次註入都是不一樣的實例。
  • Scoped
    在同一個Requset中,不論是在哪邊被註入,都是同樣的實例。
  • Singleton
    不管Requset多少次,都會是同一個實例。

參考

Introduction to Dependency Injection in ASP.NET Core
ASP.NET Core Dependency Injection Deep Dive

老司機發車啦:https://github.com/SnailDev/SnailDev.NETCore2Learning

ASP.NET Core 2 學習筆記(四)依賴註入