全面理解 ASP.NET Core 依賴注入

DI在.NET Core里面被提到了一個(gè)非常重要的位置赁温, 這篇文章主要再給大家普及一下關(guān)于依賴注入的概念坛怪,身邊有工作六七年的同事還個(gè)東西搞不清楚。另外再介紹一下.NET Core的DI實(shí)現(xiàn)以及對實(shí)例生命周期的管理(這個(gè)是經(jīng)常面試會問到的問題)束世。最后再給大家簡單介紹一下在控制臺以及Mvc下如何使用DI酝陈,以及如何把默認(rèn)的Service Container 替換成Autofac床玻。

我錄了一些關(guān)于ASP.NET Core的入門視頻:有興趣的同學(xué)可以去看看毁涉。 http://www.cnblogs.com/jesse2013/p/aspnetcore-videos.html

  • 一、什么是依賴注入

  • 1.1 依賴

  • 1.2 什么注入

  • 為什么反轉(zhuǎn)

  • 何為容器

  • 二锈死、.NET Core DI

  • 2.1 實(shí)例的注冊

  • 2.2 實(shí)例生命周期之單例

  • 2.3 實(shí)例生命周期之Tranisent

  • 2.4 實(shí)例生命周期之Scoped

  • 三贫堰、DI在ASP.NET Core中的應(yīng)用

  • 3.1 在Startup類中初始化

  • 3.2 Controller中使用

  • 3.3 View中使用

  • 3.4 通過HttpContext來獲取

  • 四、如何替換其它的Ioc容器

一待牵、什么是依賴注入(Denpendency Injection)

這也是個(gè)老身常談的問題其屏,到底依賴注入是什么? 為什么要用它缨该? 初學(xué)者特別容易對控制反轉(zhuǎn)IOC(Iversion of Control)偎行,DI等概念搞暈。

1.1依賴

當(dāng)一個(gè)類需要另一個(gè)類協(xié)作來完成工作的時(shí)候就產(chǎn)生了依賴贰拿。比如我們在AccountController這個(gè)控制器需要完成和用戶相關(guān)的注冊蛤袒、登錄 等事情。其中的登錄我們由EF結(jié)合Idnetity來完成膨更,所以我們封裝了一個(gè)EFLoginService妙真。這里AccountController就有一個(gè)ILoginService的依賴。

image

這里有一個(gè)設(shè)計(jì)原則:依賴于抽象荚守,而不是具體的實(shí)現(xiàn)珍德。所以我們給EFLoginService定義了一個(gè)接口练般,抽象了LoginService的行為。

1.2 什么是注入

注入體現(xiàn)的是一個(gè)IOC(控制反轉(zhuǎn)的的思想)锈候。在反轉(zhuǎn)之前 先较,我們先看看正轉(zhuǎn)。

AccountController自己來實(shí)例化需要的依賴果善。

private ILoginService<ApplicationUser> _loginService;
public AccountController()
{
  _loginService = new EFLoginService()
}

大師說烙荷,這樣不好。你不應(yīng)該自己創(chuàng)建它虑稼,而是應(yīng)該由你的調(diào)用者給你琳钉。于是你通過構(gòu)造函數(shù)讓外界把這兩個(gè)依賴傳給你。

public AccountController(ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

把依賴的創(chuàng)建丟給其它人蛛倦,自己只負(fù)責(zé)使用歌懒,其它人丟給你依賴的這個(gè)過程理解為注入。

1.3 為什么要反轉(zhuǎn)溯壶?

為了在業(yè)務(wù)變化的時(shí)候盡少改動代碼可能造成的問題及皂。

比如我們現(xiàn)在要把從EF中去驗(yàn)證登錄改為從Redis去讀,于是我們加了一個(gè) RedisLoginService且改。這個(gè)時(shí)候我們只需要在原來注入的地方改一下就可以了验烧。

image
public AccountController(ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

// 用Redis來替換原來的EF登錄 var controller = new AccountController(new RedisLoginService()); controller.Login(userName, password);

1.4 何為容器

上面我們在使用AccountController的時(shí)候,我們自己通過代碼創(chuàng)建了一個(gè)ILoggingServce的實(shí)例又跛。想象一下碍拆,一個(gè)系統(tǒng)中如果有100個(gè)這樣的地方,我們是不是要在100個(gè)地方做這樣的事情慨蓝? 控制是反轉(zhuǎn)了感混,依賴的創(chuàng)建也移交到了外部。現(xiàn)在的問題是依賴太多礼烈,我們需要一個(gè)地方統(tǒng)一管理系統(tǒng)中所有的依賴弧满,容器誕生了。

容器負(fù)責(zé)兩件事情:

  • 綁定服務(wù)與實(shí)例之間的關(guān)系

  • 獲取實(shí)例此熬,并對實(shí)例進(jìn)行管理(創(chuàng)建與銷毀)

image

二庭呜、.NET Core DI

2.1 實(shí)例的注冊

前面講清楚DI和Ioc的關(guān)鍵概念之后,我們先來看看在控制臺中對.NET Core DI的應(yīng)用犀忱。在.NET Core中DI的核心分為兩個(gè)組件:IServiceCollection和 IServiceProvider募谎。

  • IServiceCollection 負(fù)責(zé)注冊

  • IServiceProvider 負(fù)責(zé)提供實(shí)例

通過默認(rèn)的 ServiceCollection(在Microsoft.Extensions.DependencyInjection命名空間下)有三個(gè)方法:

var serviceCollection = new ServiceCollection()
  .AddTransient<ILoginService, EFLoginService>()
  .AddSingleton<ILoginService, EFLoginService>()
  .AddScoped<ILoginService, EFLoginService>();

這三個(gè)方法都是將我們的實(shí)例注冊進(jìn)去,只不過實(shí)例的生命周期不一樣峡碉。什么時(shí)候生命周期我們下一節(jié)接著講近哟。

ServiceCollection的默認(rèn)實(shí)現(xiàn)是提供一個(gè)ServiceDescriptor的List

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

我們上面的AddTransient、AddSignletone和Scoped方法是IServiceCollection的擴(kuò)展方法鲫寄, 都是往這個(gè)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 實(shí)例的生命周期之單例

我們上面看到了疯淫,.NET Core DI 為我們提供的實(shí)例生命周其包括三種:

  • Transient: 每一次GetService都會創(chuàng)建一個(gè)新的實(shí)例
  • Scoped: 在同一個(gè)Scope內(nèi)只初始化一個(gè)實(shí)例 ,可以理解為( 每一個(gè)request級別只創(chuàng)建一個(gè)實(shí)例戳玫,同一個(gè)http request會在一個(gè) scope內(nèi))
  • Singleton :整個(gè)應(yīng)用程序生命周期以內(nèi)只創(chuàng)建一個(gè)實(shí)例

對應(yīng)了Microsoft.Extensions.DependencyInjection.ServiceLifetime的三個(gè)枚舉值

public enum ServiceLifetime
{
  Singleton,
  Scoped,
  Transient
}

為了大家能夠更好的理解這個(gè)生命周期的概念我們做一個(gè)測試:

定義一個(gè)最基本的IOperation里面有一個(gè) OperationId的屬性熙掺,IOperationSingleton也是一樣,只不過是另外一個(gè)接口咕宿。

public interface IOperation
{
        Guid OperationId { get; }
}
public interface IOperationSingleton : IOperation { }
public interface IOperationTransient : IOperation{}
public interface IOperationScoped : IOperation{}

我們的 Operation實(shí)現(xiàn)很簡單币绩,可以在構(gòu)造函數(shù)中傳入一個(gè)Guid進(jìn)行賦值,如果沒有的話則自已New一個(gè) 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;
}

在程序內(nèi)我們可以多次調(diào)用ServiceProvider的GetService方法缆镣,獲取到的都是同一個(gè)實(shí)例。

var services = new ServiceCollection();
// 默認(rèn)構(gòu)造
services.AddSingleton<IOperationSingleton, Operation>();
// 自定義傳入Guid空值
services.AddSingleton<IOperationSingleton>(
  new Operation(Guid.Empty));
// 自定義傳入一個(gè)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 }");
image

我們對IOperationSingleton注冊了三次试浙,最后獲取兩次董瞻,大家要注意到我們獲取到的始終都是我們最后一次注冊的那個(gè)給了一個(gè)Guid的實(shí)例,前面的會被覆蓋田巴。

2.3 實(shí)例生命周期之Tranisent

這次我們獲取到的IOperationTransient為兩個(gè)不同的實(shí)例钠糊。

image

2.4 實(shí)例生命周期之Scoped

.NET Core人IServiceProvider提供CreateScope產(chǎn)生一個(gè)新的ServiceProvider范圍,在這個(gè)范圍下的Scope標(biāo)注的實(shí)例將只會是同一個(gè)實(shí)例壹哺。換句話來說:用Scope注冊的對象抄伍,在同一個(gè)ServiceProvider的 Scope下相當(dāng)于單例。

同樣我們先分別注冊IOperationScoped管宵、IOperationTransient和IOperationSingletone 這三個(gè)實(shí)例截珍,用對應(yīng)的Scoped、Transient啄糙、和Singleton生命周期笛臣。

var services = new ServiceCollection()
.AddScoped<IOperationScoped, Operation>()
.AddTransient<IOperationTransient, Operation>()
.AddSingleton<IOperationSingleton, Operation>();

接下來我們用ServiceProvider.CreateScope方法創(chuàng)建一個(gè)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 }," +
        $"transient1: {transient1.OperationId}, " +
        $"singleton1: {singleton1.OperationId}");
 
    Console.WriteLine($"scope2: { scopeobj2.OperationId }, " +
        $"transient2: {transient2.OperationId}, " +
        $"singleton2: {singleton2.OperationId}");
}

接下來

scope1: 200d1e63-d024-4cd3-88c9-35fdf5c00956, 
transient1: fb35f570-713e-43fc-854c-972eed2fae52, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 200d1e63-d024-4cd3-88c9-35fdf5c00956, 
transient2: 2766a1ee-766f-4116-8a48-3e569de54259, 
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

如果再創(chuàng)建一個(gè)新的Scope運(yùn)行云稚,

scope1: 29f127a7-baf5-4ab0-b264-fcced11d0729, 
transient1: 035d8bfc-c516-44a7-94a5-3720bd39ce57, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 29f127a7-baf5-4ab0-b264-fcced11d0729, 
transient2: 74c37151-6497-4223-b558-a4ffc1897d57, 
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

大家注意到上面我們一共得到了 4個(gè)Transient實(shí)例隧饼,2個(gè)Scope實(shí)例,1個(gè)Singleton實(shí)例静陈。

image

這有什么用燕雁?

如果在Mvc中用過Autofac的InstancePerRequest的同學(xué)就知道,有一些對象在一個(gè)請求跨越多個(gè)Action或者多個(gè)Service鲸拥、Repository的時(shí)候拐格,比如最常用的DBContext它可以是一個(gè)實(shí)例。即能減少實(shí)例初始化的消耗刑赶,還能實(shí)現(xiàn)跨Service事務(wù)的功能捏浊。(注:在ASP.NET Core中所有用到EF的Service 都需要注冊成Scoped )

而實(shí)現(xiàn)這種功能的方法就是在整個(gè)reqeust請求的生命周期以內(nèi)共用了一個(gè)Scope。

三撞叨、DI在ASP.NET Core中的應(yīng)用

3.1在Startup類中初始化

ASP.NET Core可以在Startup.cs的 ConfigureService中配置DI金踪,大家看到 IServiceCollection這個(gè)參數(shù)應(yīng)該就比較熟悉了浊洞。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ILoginService<ApplicationUser>,
      EFLoginService>();
    services.AddMvc();
)

ASP.NET Core的一些組件已經(jīng)提供了一些實(shí)例的綁定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的擴(kuò)展方法胡岔。

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中使用

一般可以通過構(gòu)造函數(shù)或者屬性來實(shí)現(xiàn)注入法希,但是官方推薦是通過構(gòu)造函數(shù)。這也是所謂的顯式依賴靶瘸。

private ILoginService<ApplicationUser> _loginService;
public AccountController(
  ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

我們只要在控制器的構(gòu)造函數(shù)里面寫了這個(gè)參數(shù)苫亦,ServiceProvider就會幫我們注入進(jìn)來。這一步是在Mvc初始化控制器的時(shí)候完成的怨咪,我們后面再介紹到Mvc的時(shí)候會往細(xì)里講屋剑。

3.3 View中使用

在View中需要用@inject 再聲明一下,起一個(gè)別名诗眨。

@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來獲取實(shí)例

HttpContext下有一個(gè)RequestedService同樣可以用來獲取實(shí)例對象饼丘,不過這種方法一般不推薦。同時(shí)要注意GetService<>這是個(gè)范型方法辽话,默認(rèn)如果沒有添加Microsoft.Extension.DependencyInjection的using肄鸽,是不用調(diào)用這個(gè)方法的。

HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();

四油啤、如何替換其它的Ioc容器

Autofac也是不錯(cuò)的選擇典徘,但我們首先要搞清楚為什么要替換掉默認(rèn)的 DI容器?益咬,替換之后有什么影響逮诲?.NET Core默認(rèn)的實(shí)現(xiàn)對于一些小型的項(xiàng)目完全夠用,甚至大型項(xiàng)目麻煩點(diǎn)也能用幽告,但是會有些麻煩梅鹦,原因在于只提供了最基本的AddXXXX方法來綁定實(shí)例關(guān)系,需要一個(gè)一個(gè)的添加冗锁。如果項(xiàng)目可能要添加好幾百行這樣的方法齐唆。

如果熟悉Autofac的同學(xué)可能會這下面這樣的代碼有映象。

builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));
  
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));

這會給我們的初始化帶來一些便利性冻河,我們來看看如何替換Autofac到ASP.NET Core箍邮。我們只需要把Startup類里面的 ConfigureService的 返回值從 void改為 IServiceProvider即可。而返回的則是一個(gè)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 有何變化

其中很大的一個(gè)變化在于锭弊,Autofac 原來的一個(gè)生命周期InstancePerRequest,將不再有效擂错。正如我們前面所說的味滞,整個(gè)request的生命周期被ASP.NET Core管理了,所以Autofac的這個(gè)將不再有效。我們可以使用 InstancePerLifetimeScope 剑鞍,同樣是有用的刹悴,對應(yīng)了我們ASP.NET Core DI 里面的Scoped。

原文地址:https://www.cnblogs.com/jesse2013/p/di-in-aspnetcore.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末攒暇,一起剝皮案震驚了整個(gè)濱河市土匀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌形用,老刑警劉巖就轧,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異田度,居然都是意外死亡妒御,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門镇饺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乎莉,“玉大人,你說我怎么就攤上這事奸笤⊥锟校” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵监右,是天一觀的道長边灭。 經(jīng)常有香客問我,道長健盒,這世上最難降的妖魔是什么绒瘦? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮扣癣,結(jié)果婚禮上惰帽,老公的妹妹穿的比我還像新娘。我一直安慰自己父虑,他們只是感情好该酗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著频轿,像睡著了一般垂涯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上航邢,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機(jī)與錄音骄蝇,去河邊找鬼膳殷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赚窃。 我是一名探鬼主播册招,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼勒极!你這毒婦竟也來了是掰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤辱匿,失蹤者是張志新(化名)和其女友劉穎键痛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匾七,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡絮短,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昨忆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丁频。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖邑贴,靈堂內(nèi)的尸體忽然破棺而出席里,到底是詐尸還是另有隱情,我是刑警寧澤拢驾,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布胁勺,位于F島的核電站,受9級特大地震影響独旷,放射性物質(zhì)發(fā)生泄漏署穗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一嵌洼、第九天 我趴在偏房一處隱蔽的房頂上張望案疲。 院中可真熱鬧,春花似錦麻养、人聲如沸褐啡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽备畦。三九已至,卻和暖如春许昨,著一層夾襖步出監(jiān)牢的瞬間懂盐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工糕档, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留莉恼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像俐银,于是被迫代替她去往敵國和親尿背。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容