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

Chasity15A 8年前發布 | 36K 次閱讀 依賴注入 ASP.NET .NET開發

來自: http://www.cnblogs.com/yanbinliu/p/5170737.html

來源https://docs.asp.net/en/latest/fundamentals/dependency-injection.htmlASP.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>();

}</pre>

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);
    }
}</pre> 

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

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

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

接口有具體類型CharacterRepository來實現,在運行時被使用。

Note:CharacterRepository類只是使用DI的普通例子,你可以對應用所有的服務使用DI,而不僅僅是“倉儲”和數據獲取類。

</div>

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();
    }
}

}</pre>

請注意,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>();</pre>

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
{
}

}</pre>

我們通過一個類來實現這些接口,接受一個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;
    }
}

}</pre>

為了演示對應用的單個請求內和不同請求內的對象生命周期,樣例包含一個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();
    }
}

}</pre>

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

</div>

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

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

請求服務和應用服務

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

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>(); }</pre>

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

</div>

 本文由用戶 Chasity15A 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!