來源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
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。
觀察請求內和請求間的OperationId哪個變化。
- Transient 服務的對象總是不同的。每個controller和service都提供一個新的實例
- Scoped的對象在一個request內是一樣的,而不同的request間是不一樣的。
- Singleton對象是一直保持不表的。
- Instance對象,對於每一個對象和request都是一樣的,也即是在ConfigureServices中所指定的對象。
請求服務和應用服務##
HttpContext中的一個ASP.NET請求中的可用服務分為兩個集合,ApplicationServices和RequestServices。
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的優勢。