本文轉自:http://www.jessetalk.cn/2017/11/06/di-in-aspnetcore/html
爲何要寫這個博客
DI在.NET Core裏面被提到了一個很是重要的位置, 這篇文章主要再給你們普及一下關於依賴注入的概念,身邊有工做六七年的同事還個東西搞不清楚。另外再介紹一下.NET Core的DI實現以及對實例生命週期的管理(這個是常常面試會問到的問題)。最後再給你們簡單介紹一下在控制檯以及Mvc下如何使用DI,以及如何把默認的Service Container 替換成Autofac。vue
1、什麼是依賴注入(Denpendency Injection)
這也是個老身常談的問題,到底依賴注入是什麼? 爲何要用它? 初學者特別容易對控制反轉IOC(Iversion of Control),DI等概念搞暈。面試
1.1依賴
當一個類須要另外一個類協做來完成工做的時候就產生了依賴。好比咱們在AccountController這個控制器須要完成和用戶相關的註冊、登陸 等事情。其中的登陸咱們由EF結合Idnetity來完成,因此咱們封裝了一個EFLoginService。這裏AccountController就有一個ILoginService的依賴。
這裏有一個設計原則:依賴於抽象,而不是具體的實現。因此咱們給EFLoginService定義了一個接口,抽象了LoginService的行爲。
1.2 什麼是注入
注入體現的是一個IOC(控制反轉的的思想)。在反轉以前 ,咱們先看看正轉。
AccountController本身來實例化須要的依賴。
private ILoginService<ApplicationUser> _loginService;
public AccountController()
{
_loginService = new EFLoginService()
}
大師說,這樣很差。你不該該本身建立它,而是應該由你的調用者給你。因而你經過構造函數讓外界把這兩個依賴傳給你。dom
private ILoginService<ApplicationUser> _loginService;
public AccountController(ILoginService<ApplicationUser> loginService)
{
_loginService = loginService;
}
把依賴的建立丟給其它人,本身只負責使用,其它人丟給你依賴的這個過程理解爲注入。ide
1.3 爲何要反轉?
爲了在業務變化的時候盡少改動代碼可能形成的問題。
好比咱們如今要把從EF中去驗證登陸改成從Redis去讀,因而咱們加了一個 RedisLoginService。這個時候咱們只須要在原來注入的地方改一下就能夠了。
var controller = new AccountController(new EFLoginService());
controller.Login(userName, password);
// 用Redis來替換原來的EF登陸
var controller = new AccountController(new RedisLoginService());
controller.Login(userName, password);
1.4 何爲容器
上面咱們在使用AccountController的時候,咱們本身經過代碼建立了一個ILoggingServce的實例。想象一下,一個系統中若是有100個這樣的地方,咱們是否是要在100個地方作這樣的事情? 控制是反轉了,依賴的建立也移交到了外部。如今的問題是依賴太多,咱們須要一個地方統一管理系統中全部的依賴,容器誕生了。
容器負責兩件事情:
2、.NET Core DI
2.1 實例的註冊
前面講清楚DI和Ioc的關鍵概念以後,咱們先來看看在控制檯中對.NET Core DI的應用。在.NET Core中DI的核心分爲兩個組件:IServiceCollection和 IServiceProvider。
經過默認的 ServiceCollection(在Microsoft.Extensions.DependencyInjection命名空間下)有三個方法:
var serviceCollection = new ServiceCollection()
.AddTransient<ILoginService, EFLoginService>()
.AddSingleton<ILoginService, EFLoginService>()
.AddScoped<ILoginService, EFLoginService>();
這三個方法都是將咱們的實例註冊進去,只不過實例的生命週期不同。何時生命週期咱們下一節接着講。
ServiceCollection的默認實現是提供一個ServiceDescriptor的List
public interface IServiceCollection : IList<ServiceDescriptor>
{
}
咱們上面的AddTransient、AddSignletone和Scoped方法是IServiceCollection的擴展方法, 都是往這個List裏面添加ServiceDescriptor。函數
private static IServiceCollection Add( IServiceCollection collection, Type serviceType, Type implementationType, ServiceLifetime lifetime)
{
var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
collection.Add(descriptor);
return collection;
}
2.2 實例的生命週期之單例
咱們上面看到了,.NET Core DI 爲咱們提供的實例生命周其包括三種:
- Transient: 每一次GetService都會建立一個新的實例
- Scoped: 在同一個Scope內只初始化一個實例 ,能夠理解爲( 每個request級別只建立一個實例,同一個http request會在一個 scope內)
- Singleton :整個應用程序生命週期之內只建立一個實例
對應了Microsoft.Extensions.DependencyInjection.ServiceLifetime的三個枚舉值
public enum ServiceLifetime
{
Singleton,
Scoped,
Transient
}
爲了你們可以更好的理解這個生命週期的概念咱們作一個測試:
定義一個最基本的IOperation裏面有一個 OperationId的屬性,IOperationSingleton也是同樣,只不過是另一個接口。
public interface IOperation
{
Guid OperationId { get; }
}
public interface IOperationSingleton : IOperation { }
public interface IOperationTransient : IOperation{}
public interface IOperationScoped : IOperation{}
咱們的 Operation實現很簡單,能夠在構造函數中傳入一個Guid進行賦值,若是沒有的話則自已New一個 Guid。測試
public class Operation : IOperationSingleton, IOperationTransient, IOperationScoped
{
private Guid _guid;
public Operation() {
_guid = Guid.NewGuid();
}
public Operation(Guid guid)
{
_guid = guid;
}
public Guid OperationId => _guid;
}
在程序內咱們能夠屢次調用ServiceProvider的GetService方法,獲取到的都是同一個實例。ui
var services = new ServiceCollection();
// 默認構造
services.AddSingleton<IOperationSingleton, Operation>();
// 自定義傳入Guid空值
services.AddSingleton<IOperationSingleton>(new Operation(Guid.Empty));
// 自定義傳入一個New的Guid
services.AddSingleton <IOperationSingleton>(new Operation(Guid.NewGuid()));
var provider = services.BuildServiceProvider();
// 輸出singletone1的Guid
var singletone1 = provider.GetService<IOperationSingleton>();
Console.WriteLine($"signletone1: {singletone1.OperationId}");
// 輸出singletone2的Guid
var singletone2 = provider.GetService<IOperationSingleton>();
Console.WriteLine($"signletone2: {singletone2.OperationId}");
Console.WriteLine($"singletone1 == singletone2 ? : { singletone1 == singletone2 }");
![](http://static.javashuo.com/static/loading.gif)
咱們對IOperationSingleton註冊了三次,最後獲取兩次,你們要注意到咱們獲取到的始終都是咱們最後一次註冊的那個給了一個Guid的實例,前面的會被覆蓋。this
2.3 實例生命週期之Tranisent
此次咱們獲取到的IOperationTransient爲兩個不一樣的實例。spa
var services = new ServiceCollection();
services.AddTransient<IOperationTransient, Operation>();
var provider = services.BuildServiceProvider();
var transient1 = provider.GetService<IOperationTransient>();
Console.WriteLine($"transient1: {transient1.OperationId}");
var transient2 = provider.GetService<IOperationTransient>();
Console.WriteLine($"transient2: {transient2.OperationId}");
Console.WriteLine($"transient1 == transient2 ? : { transient1 == transient2 }");
![](http://static.javashuo.com/static/loading.gif)
2.4 實例生命週期之Scoped
.NET Core人IServiceProvider提供CreateScope產生一個新的ServiceProvider範圍,在這個範圍下的Scope標註的實例將只會是同一個實例。換句話來講:用Scope註冊的對象,在同一個ServiceProvider的 Scope下至關於單例。
一樣咱們先分別註冊IOperationScoped、IOperationTransient和IOperationSingletone 這三個實例,用對應的Scoped、Transient、和Singleton生命週期。
var services = new ServiceCollection()
.AddScoped<IOperationScoped, Operation>()
.AddTransient<IOperationTransient, Operation>()
.AddSingleton<IOperationSingleton, Operation>();
接下來咱們用ServiceProvider.CreateScope方法建立一個Scope
var provider = services.BuildServiceProvider();
using (var scope1 = provider.CreateScope())
{
var p = scope1.ServiceProvider;
var scopeobj1 = p.GetService<IOperationScoped>();
var transient1 = p.GetService<IOperationTransient>();
var singleton1 = p.GetService<IOperationSingleton>();
var scopeobj2 = p.GetService<IOperationScoped>();
var transient2 = p.GetService<IOperationTransient>();
var singleton2 = p.GetService<IOperationSingleton>();
Console.WriteLine($"scope1: { scopeobj1.OperationId },\ntransient1: {transient1.OperationId},\nsingleton1: {singleton1.OperationId}");
Console.WriteLine($"scope2: { scopeobj2.OperationId },\ntransient2: {transient2.OperationId},\nsingleton2: {singleton2.OperationId}");
}
接下來
![](http://static.javashuo.com/static/loading.gif)
若是再建立一個新的Scope運行,
![](http://static.javashuo.com/static/loading.gif)
你們注意到上面咱們一共獲得了 4個Transient實例,2個Scope實例,1個Singleton實例。
![](http://static.javashuo.com/static/loading.gif)
這有什麼用?
若是在Mvc中用過Autofac的InstancePerRequest的同窗就知道,有一些對象在一個請求跨越多個Action或者多個Service、Repository的時候,好比最經常使用的DBContext它能夠是一個實例。即能減小實例初始化的消耗,還能實現跨Service事務的功能。(注:在ASP.NET Core中全部用到EF的Service 都須要註冊成Scoped )
而實現這種功能的方法就是在整個reqeust請求的生命週期之內共用了一個Scope。
3、DI在ASP.NET Core中的應用
3.1在Startup類中初始化
ASP.NET Core能夠在Startup.cs的 ConfigureService中配置DI,你們看到 IServiceCollection這個參數應該就比較熟悉了。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILoginService<ApplicationUser>,EFLoginService>();
services.AddMvc();
)
ASP.NET Core的一些組件已經提供了一些實例的綁定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的擴展方法。
public static IMvcBuilder AddMvc(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
var builder = services.AddMvcCore();
builder.AddApiExplorer();
builder.AddAuthorization();
AddDefaultFrameworkParts(builder.PartManager);
...
}
3.2 Controller中使用
通常能夠經過構造函數或者屬性來實現注入,可是官方推薦是經過構造函數。這也是所謂的顯式依賴。
private ILoginService<ApplicationUser> _loginService;
public AccountController(ILoginService<ApplicationUser> loginService)
{
_loginService = loginService;
}
咱們只要在控制器的構造函數裏面寫了這個參數,ServiceProvider就會幫咱們注入進來。這一步是在Mvc初始化控制器的時候完成的,咱們後面再介紹到Mvc的時候會往細裏講。
3.3 View中使用
在View中須要用@inject 再聲明一下,起一個別名。
@using MilkStone.Services;
@model MilkStone.Models.AccountViewModel.LoginViewModel
@inject ILoginService<ApplicationUser> loginService
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
@loginService.GetUserName()
</body>
</html>
3.4 經過 HttpContext來獲取實例
HttpContext下有一個RequestedService一樣能夠用來獲取實例對象,不過這種方法通常不推薦。同時要注意GetService<>這是個範型方法,默認若是沒有添加Microsoft.Extension.DependencyInjection的using,是不用調用這個方法的。
HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();
4、如何替換其它的Ioc容器
Autofac也是不錯的選擇,但咱們首先要搞清楚爲何要替換掉默認的 DI容器?,替換以後有什麼影響?.NET Core默認的實現對於一些小型的項目徹底夠用,甚至大型項目麻煩點也能用,可是會有些麻煩,緣由在於只提供了最基本的AddXXXX方法來綁定實例關係,須要一個一個的添加。若是項目可能要添加好幾百行這樣的方法。
若是熟悉Autofac的同窗可能會這下面這樣的代碼有映象。
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
這會給咱們的初始化帶來一些便利性,咱們來看看如何替換Autofac到ASP.NET Core。咱們只須要把Startup類裏面的 ConfigureService的 返回值從 void改成 IServiceProvider便可。而返回的則是一個AutoServiceProvider。
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 new AutofacServiceProvider(container);
}
4.1 有何變化
其中很大的一個變化在於,Autofac 原來的一個生命週期InstancePerRequest,將再也不有效。正如咱們前面所說的,整個request的生命週期被ASP.NET Core管理了,因此Autofac的這個將再也不有效。咱們可使用 InstancePerLifetimeScope ,一樣是有用的,對應了咱們ASP.NET Core DI 裏面的Scoped。