ASP.NET Core 1.0基礎之依賴注入


來源https://docs.asp.net/en/latest/fundamentals/dependency-injection.html
ASP.NET Core 1.0在設計上原生就支持和有效利用依賴注入。在Startup類中,應用可以通過將框架內嵌服務注入到方法中來使用他們;另一方面,你也可以配置服務來注入使用。默認的服務容器只提供了最小的特性集合,所以並不打算取代其他的IoC容器。

什么是依賴注入DI##

依賴注入是為了達到解耦對象和其依賴的一項技術。一個類為了完成自身某些操作所需的對象是通過某種方式提供的,而不是使用靜態引用或者直接實例化。通常情況下,類通過構造器來聲明其依賴,遵循顯式依賴原則。這種方式稱作構造器注入。

當以DI思想來設計類時,這些類更加松耦合,因為他們不直接硬編碼的依賴其合作者。這遵循了依賴倒置原則,即高層模塊不應依賴底層模塊,兩者都應依賴抽象。類在構建時所需是抽象(如接口interface),而不是具體的實現。把依賴抽離成接口,把這些接口的實現作為參數也是策略設計模式的例子。

當一個系統使用DI來設計時,很多類通過構造器或者屬性來添加依賴,這樣就很方便有一個專門的類來創建這些類以及他們相關的依賴。這樣的類稱之為“容器”或者“IoC容器”或“DI容器”。一個容器本質上是一個工廠,來給請求者提供類型實例。如果給定類型聲明了自身依賴,容器也配置了來提供這些依賴類型,那么它會創建這些依賴作為請求實例的一部分。通過這種方式可以為 類提供復雜的依賴圖,而不需要任何硬編碼的對象依賴。除了創建依賴對象外,容器一般還管理應用內的對象生命周期。

ASP.NET Core 1.0提供了一個簡單的內置容器(以IServiceProvider為代表),默認支持構造器注入,這樣ASP.NET可以通過DI使某些服務可用。ASP.NET把它所管理的類型稱之為服務。本文的剩下部分,服務即指ASP.NET IoC容器所管理的類型。你可以在Startup類中的ConfigureServices 方法中配置內置的容器服務。
Note: Martin Fowler寫過一篇很詳細的依賴反轉的文章。微軟對此也有很棒的描述連接

使用框架提供的服務##

ConfigureServices方法負責定義應用使用的服務,包括平台特性服務如EF和ASP.NET MVC。最初提供給ConfigureServices的IServiceCollection只有少數服務。默認web模板提供了怎么通過擴展方法來添加額外服務到容器的例子,如AddEntityFramework, AddIdentity, 和AddMVC。

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddEntityFramework()
        .AddSqlServer()
        .AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
}

ASP.NET提供的特性和中間件遵循使用一個AddService擴展方法的約定,來注冊該特性使用的所需的所有服務。

Note:你可以在Startup方法中請求某些framework-provided服務,詳見應用啟動Application Startup
當然,除了配置框架提供的各種服務,你也可以配置自己定義的服務。

注冊自定義服務##

在默認web模板中,有如下兩個服務被添加到IServiceCollection中

// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

AddTransient方法將抽象類型映射為實體服務,對於每個請求這都單獨實例化,這稱作服務的生命周期。額外生命周期選項如下。對於每個注冊的服務選擇合適的生命周期是很重要的。是對每個請求類都提供一個新的實例化服務?還是在給定web請求內只實例化一次?還是在應用周期內只有單例?

在本文的例子中,有個簡單的CharacterController來顯示Character姓名,在Index方法中顯示已存儲的Character(如果沒有則創建)。雖然注冊了EF服務,但本例持久化沒有使用數據庫。具體的數據獲取服務抽象到了ICharacterRepository接口實現中,這遵從了倉儲模式。在構造器中請求ICharacterRepository參數,並將其賦給私有變量,來根據需要獲取Character。

using System.Linq;
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Models;
using Microsoft.AspNet.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class CharactersController : Controller
    {
        private readonly ICharacterRepository _characterRepository;

        public CharactersController(ICharacterRepository characterRepository)
        {
            _characterRepository = characterRepository;
        }

        // GET: /characters/
        public IActionResult Index()
        {
            var characters = _characterRepository.ListAll();
            if (!characters.Any())
            {
                _characterRepository.Add(new Character("Darth Maul"));
                _characterRepository.Add(new Character("Darth Vader"));
                _characterRepository.Add(new Character("Yoda"));
                _characterRepository.Add(new Character("Mace Windu"));
                characters = _characterRepository.ListAll();
            }

            return View(characters);
        }
    }

接口ICharacterRepository只簡單定義了兩個方法,Controller通過其來操作Charcter實例。

using System.Collections.Generic;
using DependencyInjectionSample.Models;

namespace DependencyInjectionSample.Interfaces
{
    public interface ICharacterRepository
    {
        IEnumerable<Character> ListAll();
        void Add(Character character);
    }
}

接口有具體類型CharacterRepository來實現,在運行時被使用。
Note: CharacterRepository類只是使用DI的普通例子,你可以對應用所有的服務使用DI,而不僅僅是“倉儲”和數據獲取類。

using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Models
{
    public class CharacterRepository : ICharacterRepository
    {
        private readonly ApplicationDbContext _dbContext;

        public CharacterRepository(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IEnumerable<Character> ListAll()
        {
            return _dbContext.Characters.AsEnumerable();
        }

        public void Add(Character character)
        {
            _dbContext.Characters.Add(character);
            _dbContext.SaveChanges();
        }
    }
}

請注意,CharacterRepository在其構造器中請求了ApplicationDbContext實例。這種鏈式的依賴注入是很常見的,被依賴本身又有自己的依賴。容器來負責以樹形的方式來解析所有這些依賴,並返回解析完成的服務。

Note: 創建請求對象,以及其依賴,其依賴的依賴,有時這被稱之為對象圖。同樣的,需要解析的對象集合稱之為依賴樹或者依賴圖

在本例中,ICharacterRepository和ApplicationDbContext都必須在ConfigureServices中注冊。ApplicationDbContext是通過擴展方法AddEntityFramework來配置,它包括添加DbContext (AddDbContext )的一個擴展。倉儲的注入在在ConfigureServices方法的結尾。

services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddScoped<ICharacterRepository, CharacterRepository>();

// Show different lifetime options
services.AddTransient<IOperationTransient, Operation>();

EF contexts需要使用scoped生命周期來添加到服務容器。如果你使用了上面的helper方法,這是已經處理好的。使用EF的倉儲服務應該使用同樣的生命周期。

警告:主要不安全的來源是通過單例來解析Scoped生命周期服務服務。這樣做的后果,很有可能在處理后續請求時使用的服務的狀態是錯誤的。

服務生命周期和注冊選項##

ASP.NET 服務可以配置如下生命周期:

  • Transient: Transient服務在每次被請求時都會被創建。這種生命周期比較適用於輕量級的無狀態服務。
  • Scoped: Scoped生命周期的服務是每次web請求被創建。
  • Singleton: Singleton生命能夠周期服務在第一被請求時創建,在后續的每個請求都會使用同一個實例。如果你的應用需要單例服務,推薦的做法是交給服務容器來負責單例的創建和生命周期管理,而不是自己來走這些事情。
  • Instance: 你也可以選擇直接添加實例到服務容器。如果這樣做,該實例會被后續的所有請求所使用(這樣就會創建一個scoped-Singleton實例)。Instance和Singleton的一個主要區別在於,Instance服務是由ConfigureServices創建,然而Singleton服務是lazy-loaded,在第一個被請求時才會被創建。

服務可以通過若干種方式注冊到容器。我們已經看到,對於給定類型通過指定具體類型來注冊服務的實現。除此之外,也可以指定一個工廠,用來按需創建實例。第三種方法是直接指定要使用的類型實例,在這種方式下,容器自身不會嘗試去創建實例。

為了演示這四種不同的生命周期和注冊選項,考慮一個簡單接口,代表這一個或多個任務操作,並且含有一個唯一標識符OperationId。根據我們如何配置服務的生命周期,容器對請求類或者提供同一個實例或者不同實例。為了弄明白生命周期是如何請求的,對於每一個生命周期類型我們都創建一個類型。

using System;

namespace DependencyInjectionSample.Interfaces
{
    public interface IOperation
    {
        Guid OperationId { get; }
    }

    public interface IOperationTransient : IOperation
    {
    }
    public interface IOperationScoped : IOperation
    {
    }
    public interface IOperationSingleton : IOperation
    {
    }
    public interface IOperationInstance : IOperation
    {
    }
}

我們通過一個類來實現這些接口,接受一個Guid作為構造器參數,或者使用new Guid來提供(如果沒有提供的話)。
接下來在ConfigureServices中,根據類型的生命周期來添加到容器中

services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddInstance<IOperationInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();

注意到對於Instance生命周期的實例,我們是自己提供了已知的Guid.Empty標識符,這樣我們能在該實例被使用時識別它。我們也注冊了一個OperationService,它依賴其他Operation類型。這樣我們就能弄清楚在一個請求內,對於每個類型我們是得到同樣的實例還是一個新的實例。

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
    public class OperationService
    {
        public IOperationTransient TransientOperation { get; private set; }
        public IOperationScoped ScopedOperation { get; private set; }
        public IOperationSingleton SingletonOperation { get; private set; }
        public IOperationInstance InstanceOperation { get; private set; }

        public OperationService(IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationInstance instanceOperation)
        {
            TransientOperation = transientOperation;
            ScopedOperation = scopedOperation;
            SingletonOperation = singletonOperation;
            InstanceOperation = instanceOperation;
        }
    }
}

為了演示對應用的單個請求內和不同請求內的對象生命周期,樣例包含一個OperationController依賴每種類型的Operation以及OperationService。Index方法顯示所有的服務Id。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNet.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class OperationsController : Controller
    {
        private readonly OperationService _operationService;
        private readonly IOperationTransient _transientOperation;
        private readonly IOperationScoped _scopedOperation;
        private readonly IOperationSingleton _singletonOperation;
        private readonly IOperationInstance _instanceOperation;

        public OperationsController(OperationService operationService,
            IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationInstance instanceOperation)
        {
            _operationService = operationService;
            _transientOperation = transientOperation;
            _scopedOperation = scopedOperation;
            _singletonOperation = singletonOperation;
            _instanceOperation = instanceOperation;
        }

        public IActionResult Index()
        {
            ViewBag.Transient = _transientOperation;
            ViewBag.Scoped = _scopedOperation;
            ViewBag.Singleton = _singletonOperation;
            ViewBag.Instance = _instanceOperation;
            ViewBag.Service = _operationService;
            return View();
        }
    }
}

然后有兩個請求到達controller action。
index1
index2

觀察請求內和請求間的OperationId哪個變化。

  • Transient 服務的對象總是不同的。每個controller和service都提供一個新的實例
  • Scoped的對象在一個request內是一樣的,而不同的request間是不一樣的。
  • Singleton對象是一直保持不表的。
  • Instance對象,對於每一個對象和request都是一樣的,也即是在ConfigureServices中所指定的對象。

請求服務和應用服務##

HttpContext中的一個ASP.NET請求中的可用服務分為兩個集合,ApplicationServicesRequestServices
service1

Request services作為應用的一部分是你可以配置和request的。而Application Services則是局限於在在應用啟動(Startup)時可用的服務。Scoped的服務是作為Request Services的一部分而不是Applocation Services的一部分。當對象指定依賴時,是由RequestServices中的類型所提供,而不是ApplicationServices。

一般來將你不應該直接使用這些屬性,而是傾向於通過類的構造器來請求這些類型,讓框架來注入這些依賴。這樣產生的類更容易測試和更松耦合。

Note: 需要重點記住的是,應用幾乎總是會使用RequestServices,任何情況下你都不應該直接使用這些屬性。而是通過構造器來請求所需服務。

自定義依賴注入服務##

你可以設計自己的服務並通過依賴注入到需求方。這樣可以避免使用有狀態的靜態方法調用(會導致code smell,即static cling)和服務內對依賴類的直接實例化。當選擇是否通過New來實例化一個類型或者通過依賴注入來氫氣,記住“New is Glue”也許是點幫助的。通過遵循Solid面向對象設計原則,設計的類自然就會small, well-factored,和easily tested。

如果你發現類有了太多需要注入的依賴怎么辦?一般來說,這是類承擔了太多職責的標志,很有可能違反了SRP(單一職責原則)。檢查是否能把其中的某些職責轉到新的類。記住,Controller類應該只關注UI,所以業務規則和數據獲取實現應該放在合適的關注點分離的類中。

至於數據獲取,你可以注入EF DbContext類型到Controller中(假設你已經在Startup類中配置了EF)。然而,一般避免在UI項目中直接依賴DbContext類型,而是依賴抽象,如Repository接口,在接口的實現中限制EF的相關知識。這樣會減少項目和數據訪問策略的耦合,使得測試代碼更加容易。

取代默認服務容器##

內建的服務容器只滿足框架最基本的需求,大部分的consumer 應用基於此構建。然而,如果開發者希望取代內建的容器,使用自己偏好的容器,也是可以很容易做到的。ConfigureServices方法一般返回void,但是如果返回類型簽名改為IServiceProvider,就可以配置和返回別的容器。有很多IoC .NET容器。當它們可用時,本文會添加這些容器的DNX實現。在本例中,使用Autofac包。

首先在project.json中添加合適的容器包。

"dependencies" : {
  "Autofac": "4.0.0-rc1",
  "Autofac.Extensions.DependencyInjection": "4.0.0-rc1"
},

然后在ConfigureServices中配置容器,並返回IServiceProvider:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  // add other framework services

  // Add Autofac
  var containerBuilder = new ContainerBuilder();
  containerBuilder.RegisterModule<DefaultModule>();
  containerBuilder.Populate(services);
  var container = containerBuilder.Build();
  return container.Resolve<IServiceProvider>();
}

Note: 當使用第三方DI容器時,需要改變ConfigreServices的返回簽名,改成IServiceProvider而不是void。

最后在DefaultModule中配置Autofac。

public class DefaultModule : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
  }
}

然后在運行時,Autofac將會解析類型和注入的依賴

Package(Nuget) ProjectSite
Autofac.Dnx http://autofac.org
StructureMap.Dnx http://structuremap.github.io
##推薦規范## 當使用依賴注入時,記住如下推薦規范:
  • DI是針對於有復雜依賴的對象。Controllers, services, adapters和 repositories都是一些可以添加依賴的對象的例子。
  • 避免直接在DI中存儲數據和配置。例如,用戶購物車不應添加到服務容器中。配置應該使用Options Model中文鏈接。類似的,避免“data holder” objects that only exist to allow access to some other object。如果可能盡量通過DI來獲取實際的item。
  • 避免靜態獲取服務
  • 在應用代碼中避免service location
  • 避免靜態獲取HttpContext

Note: 如上所有推薦規范,你可能遇到必須忽略某條的情形。但是這種情形很少,而且基本都是框架本身內部的情形。
記住,依賴注入是static/global對象獲取模式的一個替代方式。如果你把DI和靜態對象接入混用,你可能不能體會到DI的優勢。

Download sample from GitHub


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM