学习ASP.NET Core,你必须了解无处不在的“依赖注入”

ASP.NET
Core的核心是通过一个Server和若干注册的Middleware构成的管道,不论是管道自身的构建,还是Server和Middleware自身的实现,以及构建在这个管道的应用,都需要相应的服务提供支持,ASP.NET
Core自身提供了一个DI容器来实现针对服务的注册和消费。换句话说,不只是ASP.NET
Core底层框架使用的服务是由这个DI容器来注册和提供,应用级别的服务的注册和提供也需要以来这个DI容器,所以正如本文标题所说的——学习ASP.NET
Core,你必须了解无处不在的“依赖注入”。

目录一、依赖注入简介
二、依赖注入在管道构建过程中的应用
三、依赖服务的注册与注入
四、让Startup的ConfigureServices方法返回一个ServiceProvider
五、ASP.NET Core默认注册了哪些服务
六、ASP.NET Core MVC中的依赖注入

一、依赖注入简介

说到依赖注入(Dependency Injection,以下简称DI),就必须说IoC(Inverse of Control),很多人将这两这混为一谈,其实这是两个完全不同的概念,或者是不同“层次”的两个概念,我曾在《控制反转(IoC)》和《依赖注入(DI)》对这两个概念做过详细介绍。ASP.NET

Core使用的DI框架由“Micorosoft.Extensions.DependencyInjection”这个NuGet包来承载,我们也可以非ASP.NET
Core应用或者你自己的框架上单独使用它,对于这个DI框架的设计、实现以及编程相关的内容,我在系列文章《ASP.NET Core 中的依赖注入 [共7篇]》对此有过详细的介绍。

DI框架具有两个核心的功能,即服务的注册和提供,这两个功能分别由对应的对象来承载,

它们分别是ServiceCollection和ServiceProvider。如下图所示,我们将相应的服务以不同的生命周期模式(Transient、Scoped和Singleton)注册到ServiceCollection对象之上,在利用后者创建的ServiceProvider根据注册的服务类型提取相应的服务对象。

二、依赖注入在管道构建过程中的使用

在ASP.NET

Core管道的构架过程中主要涉及三个对象/类型,作为宿主的WebHost和他的创建者WebHostBuilder,以及注册到WebHostBuilder的Startup类型。
如下的代码片段体现了启动ASP.NET
Core应用采用的典型编程模式:我们首先创建一个WebHostBuilder对象,并将采用Server和Startup类型注册到它之上。在调用Build方法创建WebHost之前,我们还可以调用相应的方式做其他所需的注册工作。当我们调用WebHost的Run方法之后,后者会利用注册的Startup类型来构建完整的管道。那么在管道的构建过程中,DI是如何被应用的呢?

   1: new WebHostBuilder()
   2:     .UseKestrel()
   3:     .UseStartup<Startup>()
   4:     .Xxx
   5:     .Build()
   6:     .Run()

DI在管道ASP.NET
Core管道构建过程中的应用基本体现下面这个序列图中。当我们调用WebHostBuilder的Build方法创建对应的WebHost的时候,前者会创建一个ServiceCollection对象,并将一系列预定义的服务注册在它之上。接下来WebHostBuilder会利用这个ServiceCollection对象创建出对应的ServieProvider,这个ServiceProvider和ServiceCollection对象会一并传递给最终创建WebHost对象。当我们调用WebHost的Run方法启动它的时候,如果注册的Startup是一个实例类型,它会利用这个ServiceProvider以构造器注入的方式创建对应的Startup对象。说的具体一点,我们注册的Startup类型的构造函数是允许定义参数的,但是参数类型必须是预先注册到ServiceCollection中的服务类型。

注册的Startup方法可以包含一个可选的ConfigureServices方法,这个方法具有一个类型为IServiceCollection接口的参数。WebHost会将WebHostBuilder传递给它的ServiceCollection作为参数调用这个ConfigureServices方法,而我们则利用这个方法将注册的中间件和应用所需的服务注册到这个ServiceCollection对象上。在这之后,所有需要的服务(包括框架和应用注册的服务)都注册到这个ServiceCollection上面,WebHost会利用它创建一个新的ServiceProvider。WebHost会利用这个ServiceProvider对象以方法注入的方式调用Startup对象/类型的Configure方法,最终完成你对整个管道的建立。换句话会说,定义在Startup类型中旨在用于注册Middleware的Configure方法除了采用IApplicationBuilder作为第一个参数之外,它依然可以采用注册的任何一个服务类型作为后续参数的类型。

服务的注册除了是现在注册的Startup类型的ConfigureServices方法之外,实际上还具有另一个实现方式,那就是调用WebHostBuilder具有如下定义的ConfigureServices方法。当WebHostBuilder创建出ServiceCollection对象并完成了默认服务的注册后,我们通过调用这个方法所传入的所有Action<IServiceCollection>对象将最终应用到这个ServiceCollection对象上。

   1: public interface IWebHostBuilder
   2: {
   3:     IWebHostBuilder ConfigureServiecs(Action<IServiceCollection> configureServices);
   4: }

值得一提的是,Startup类型的ConfigureServices方法是允许具有一个IServiceProvider类型的返回值,如果这个方法返回一个具体的ServiceProrivder,那么WebHost将不会利用ServiceCollection来创建ServiceProvider,而是直接使用这个返回的ServiceProvider来调用Startup对象/类型的Configure方法。这实际上是一个很有用的扩展点,我们使用它可以实现针对其它DI框架的集成。

三、依赖服务的注册与注入

接下来我们通过一个实例来演示如何利用Startup类型的ConfigureServices来注册服务,以及发生在Startup类型上的两种依赖注入形式。如下面的代码片段所示,我们定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar)。其中其中服务Foo是通过调用WebHostBuilder的ConfigureServices方法进行注册的,而另一个服务Bar的注册则发生在Startup的ConfigureServices方法上。对于Startup来说,它具有一个类型为IFoo的只读属性,该属性在构造函数利用传入的参数进行初始化,不用说这体现了针对Startup的构造器注入。Startup的Configure方法除了ApplicationBuilder作为第一个参数之外,还具有另一个类型为IBar的参数,我们利用它来演示方法注入。

   1: public interface IFoo { }
   2: public interface IBar { }
   3: public class Foo : IFoo { }
   4: public class Bar : IBar { }
   5:  
   6: public class Program
   7: {
   8:     public static void Main(string[] args)
   9:     {
  10:         new WebHostBuilder()
  11:             
  12:             .UseKestrel()
  13:             .UseStartup<Startup>()
  14:             .Build()
  15:             .Run();
  16:     }
  17: }
  18: public class Startup
  19: {
  20:     public IFoo Foo { get; private set; }
  21:     public Startup()
  22:     {
  23:         this.Foo = foo;
  24:     }    
  25:     public void ConfigureServices(IServiceCollection services)
  26:     {
  27:         
  28:     }
  29:     
  30:     public void Configure(IApplicationBuilder app, IBar bar)
  31:     {
  32:         app.Run(async context =>
  33:         {
  34:             context.Response.ContentType = "text/html";
  35:             await context.Response.WriteAsync($"IFoo=>{this.Foo}<br/>");
  36:             await context.Response.WriteAsync($"IBar=>{bar}");
  37:         });
  38:     }
  39: }

在Startup的Configure方法中,我们调用ApplicationBulder的Run方法注册了一个Middleware,后者将两个注入的服务的类型作为响应的内容。当我们运行这个应用,并利用浏览器访问默认的监听地址(http://localhost:5000)时,浏览器会将注入的两个服务对象的类型以下图的方式展现出来。

四、让Startup的ConfigureServices方法返回一个ServiceProvider

我们说注册的Startup类型的ConfigureServices允许返回一个ServiceProvider,这个特性的重要意义在于它使我们可以实现与第三方DI框架(比如Unity、Castle、Ninject和AutoFac等)的集成。我们照例采用一个实例对此做一个演示,简单起见,我们并不会真正利用某个具体的DI框架来创建这个ServiceProvider,而是直接创建一个新的ServiceCollection来创建它,为此我们对上面这个程序进行了如下的改写。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseStartup<Startup>()
   8:             .Build()
   9:             .Run();
  10:     }
  11: }
  12: public class Startup
  13: {   
  14:     public IServiceProvider ConfigureServices(IServiceCollection services)
  15:     {
  16:         IServiceCollection newServices = new ServiceCollection();
  17:         foreach (ServiceDescriptor service in services)
  18:         {
  19:             newServices.Add(service);
  20:         }
  21:  
  22:         return newServices
  23:             .AddSingleton<IFoo, Foo>()
  24:             .AddSingleton<IBar, Bar>()
  25:             .BuildServiceProvider();
  26:     }
  27:     
  28:     public void Configure(IApplicationBuilder app, IFoo foo, IBar bar)
  29:     {
  30:         app.Run(async context =>
  31:         {
  32:             context.Response.ContentType = "text/html";
  33:             await context.Response.WriteAsync($"IFoo=>{foo}<br/>");
  34:             await context.Response.WriteAsync($"IBar=>{bar}");
  35:         });
  36:     }
  37: }

如上面的代码片段所示,在Startup的ConfigureServices方法中,我们通过拷贝注册到现有ServiceCollection的所有ServiceDescriptor生成了一个新的ServiceCollection,两个服务Foo和Bar被注册到后者之上。该方法最终返回由这个新ServiceCollection创建的ServiceProvider。在另一个Configure方法中,我们添加了两个类型分别为IFoo和IBar的参数,并以相同的方式将它们的真实类型名称和注册服务类型的映射关系作为响应内容。程序运行之后,我们利用浏览器进行访问照样会得到一样的结果。

五、ASP.NET Core默认注册了哪些服务

WebHostBuilder在创建ServiceCollection之后,会注册一些默认的服务。这些服务和我们自行注册的服务并没有任何区别,只要我们知道对应的服务类型,就可以通过注入的方式获取并使用它们。那么具体由哪些服务被默认注册了呢?如下所示的是这些服务对应的类型,至于这些服务各自有何用途,我们在这里就先不深究了。

  • IHostingEnvironment
  • ILoggerFactory
  • ILogger<>
  • IApplicationBuilderFactory
  • IHttpContextFactory
  • IOptions<>
  • DiagnosticSource
  • DiagnosticListener
  • IStartupFilter
  • ObjectPoolProvider
  • IStartup

如果我们需要这些预注册的服务,我们可以按照我们熟悉的方式以依赖注入的方式来使用它们。如下面的代码片段所示,我们在Startup的Configure方法中直接采用方法注入的方式来使用这些预定义的服务。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseStartup<Startup>()
   8:             .Build()
   9:             .Run();
  10:     }
  11: }
  12: public class Startup
  13: {     
  14:     public void Configure(
  15:         IApplicationBuilder app,
  16:         IHostingEnvironment environment,
  17:         ILoggerFactory loggerFactory,
  18:         IHttpContextFactory httpContextFactory,
  19:         DiagnosticSource diagnosticSource,
  20:         DiagnosticListener diagnosticListener)
  21:     {
  22:         app.Run(async context =>
  23:         {
  24:             context.Response.ContentType = "text/html";
  25:             await context.Response.WriteAsync($"IApplicationBuilder=>{app}<br/>");
  26:             await context.Response.WriteAsync($"IHostingEnvironment=>{environment}<br/>");
  27:             await context.Response.WriteAsync($"ILoggerFactory=>{loggerFactory}<br/>");
  28:             await context.Response.WriteAsync($"IHttpContextFactory=>{httpContextFactory}<br/>");
  29:             await context.Response.WriteAsync($"DiagnosticSource=>{diagnosticSource}<br/>");
  30:             await context.Response.WriteAsync($"DiagnosticListener=>{diagnosticListener}");
  31:         });
  32:     }
  33: }

由于Configure方法注册的Middleware直接将注入服务的注册类型和真实类型的映射关系作为响应内容,所以我们访问应用会的得到如下所示的输出结果。

六、ASP.NET Core MVC中的依赖注入

对于ASP.NET MVC
5机器以及之前的版本,在默认情况下定义的Controller都具有一个要求,那就是Controller类型必须具有一个无参数的默认构造函数,否则Controller实例将无法激活。对于自身具有依赖注入功能的ASP.NET
Core
MVC来说,定义Controller将没有了这个限制。对于预注册的服务,我们完全可以采用构造器注入的方式在定义的Controller中使用它们。作为演示,我们对上面这个应用作了如下的改写。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             
   8:                 
   9:                 
  10:                 
  11:             
  12:             .Build()
  13:             .Run();
  14:     }
  15: }
  16:  
  17: public class HomeController
  18: {
  19:     public IFoo Foo { get; private set; }
  20:     public IBar Bar { get; private set; }
  21:  
  22:     public HomeController(IFoo foo, IBar bar)
  23:     {
  24:         this.Foo = foo;
  25:         this.Bar = bar;
  26:     }
  27:  
  28:     [HttpGet("/")]
  29:     public string Index()
  30:     {
  31:         this.HttpContext.Response.ContentType = "text/html";
  32:         return $"IFoo=>{this.Foo}<br/>IBar=>{this.Bar}";
  33:     }       
  34: }

上面这个代码与之前有一个显著的区别,那就是我们根本就没有定义Startup类型,我们将原本实现在它的两个方法(ConfigureServices和Configure)中的功能移植到了WebHostBuilder的同名方法中,这两种形式的编程方式其实是等效的。在调用ConfigureServices方法的时候,我们除了注册MVC相关的服务之外,Foo和Bar这两个服务也一并进行了注册。至于另一个Configure方法,我们直接调用其扩展方法MVC注册与MVC相关的Middleware。

我们定义了一个默认的HomeController,它具有两个类型分别为IFoo和IBar的只读属性,后者在构造函数由传入的参数进行初始化,我们知道这是构造器注入的编程方式。在Action方法Index中
,我们依然将这两个服务的注册类型和真实类型之间的匹配关系作为响应内容,所以我们访问这个应用依然会得到如下所示的输出结果。

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2017-10-25

学习ASP.NET Core,你必须了解无处不在的“依赖注入”的相关文章

学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 "通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程"(上篇.中篇.下篇) 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程.在本系列 中,我们会还原构建模拟管道时可以舍弃和改写的部分,向读者朋友们呈现一个真是的HTTP请求处理管道. ASP.NET Core 的请求处理管道由一个服务器与一组有序排列的中间件构成

ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起

我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内容相对分散和零碎,我们有必要针对这个主题作一个归纳性的介绍.采用依赖注入的服务均由某个ServiceProvider来提供,但是在ASP.NET Core管道涉及到两个不同的ServiceProvider,其中一个是在管道成功构建后创建并绑定到WebHost上的ServiceProvider,对应着

ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法

在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET Core应用中基于依赖注入的编程主要涉及到两个方面,它们分别是将服务注册到ServiceCollection中,和采用注入的方式利用ServiceProvider提供我们所需的服务.我们先来讨论ASP.NET Core应用中如何进行服务注册.[本文已经同步到<ASP.NET Core框架揭秘>之中

解决ASP.NET Core Mvc文件上传限制问题实例_实用技巧

一.简介 在ASP.NET Core MVC中,文件上传的最大上传文件默认为20MB,如果我们想上传一些比较大的文件,就不知道怎么去设置了,没有了Web.Config我们应该如何下手呢? 二.设置上传文件大小 1.应用程序级别设置 我们需要在 ConfigureServices方法中添加如下代码,设置文件上传的大小限制为60 MB. public void ConfigureServices(IServiceCollection services) { servicesConfigure<For

怎么学习asp 给非专业的asp爱好者

爱好者|爱好者 首先声明, 1 本人不是什么asp高手,本人只是菜鸟 2 本文不是给程序员和高手看的,是给喜欢asp的业余爱好者共同探讨的 3 本人的学习方法并不一定正确,请自行斟酌 本人毕业于山东医科大学,五年的临床医学,出来以后干了医生,后来学习asp,想起学习的时候,那些日子是比较苦,但是挺有意思. 开始的时候学习程序只是爱好,学习asp已经将近两年了,这两年里,没有老师,只有一本从书店里买的书,可是因为没有电脑程序基础,看来看去什么也看不懂.最后一咬牙,从网上当了几个asp程序,然后从头

关于学习ASP和编程的28个观点

编程   1.不要放过任何一个看上去很简单的小编程问题--他们往往并不那么简单,或者可以引伸出很多知识点: 2.会用asp,并不说明你会asp: 3.看asp的书,是学不了asp语言的: 4.浮躁的人容易说:asp语言不行了,应该学yy:--是你自己不行了吧!? 5.浮躁的人容易问:我到底该学什么:--别问,学就对了: 6.浮躁的人容易问:asp有钱途吗:--建议你去抢银行: 7.浮躁的人容易说:我要中文版!我英文不行!--不行?学呀! 8.浮躁的人容易问:asp和yy哪个好:--告诉你吧,都好

我是怎么学习asp的 给非专业的asp爱好者

爱好者 说明,同样适合于我们PHPer啊! 首先声明, 1 本人不是什么asp高手,本人只是菜鸟 2 本文不是给程序员和高手看的,是给喜欢asp的业余爱好者共同探讨的 3 本人的学习方法并不一定正确,请自行斟酌 本人毕业于山东医科大学,五年的临床医学,出来以后干了医生,后来学习asp,想起学习的时候,那些日子是比较苦,但是挺有意思. 开始的时候学习程序只是爱好,学习asp已经将近两年了,这两年里,没有老师,只有一本从书店里买的书,可是因为没有电脑程序基础,看来看去什么也看不懂.最后一咬牙,从网上

ASP教程:深入认识学习ASP内置对象Request

request|对象|教程|内置对象 系统学习ASP,就是先从ASP的几大内置对象开始的.一般称五大对象:Request.Response.Server.Session.Application.今天先来看看Request对象. 当然一直还没有提到的就是,ASP到底是什么样?我看代码怎么知道是ASP代码?很简单,当看到"<%"和"%>"就表明是ASP,并且两者之间的就是ASP源码. 那为什么要学对象,对象的作用又是如何的? 其实ASP所提供的这些可在脚本中

深入认识学习ASP内置对象Request

系统学习ASP,就是先从ASP的几大内置对象开始的.一般称五大对象:Request.Response.Server.Session.Application.今天先来看看Request对象. 当然一直还没有提到的就是,ASP到底是什么样?我看代码怎么知道是ASP代码?很简单,当看到"<%"和"%>"就表明是ASP,并且两者之间的就是ASP源码. 那为什么要学对象,对象的作用又是如何的? 其实ASP所提供的这些可在脚本中使用的内建对象,使用户更容易收集通过浏

于学习ASP和编程的28个观点

编程 1.不要放过任何一个看上去很简单的小编程问题--他们往往并不那么简单,或者可以引伸出很多知识点: 2.会用,并不说明你会: 3.看asp的书,是学不了语言的: 4.浮躁的人容易说:语言不行了,应该学yy:--是你自己不行了吧!? 5.浮躁的人容易问:我到底该学什么:--别问,学就对了: 6.浮躁的人容易问:asp有钱途吗:--建议你去抢银行: 7.浮躁的人容易说:我要中文版!我英文不行!--不行?学呀! 8.浮躁的人容易问:asp和yy哪个好:--告诉你吧,都好--只要你学就行: 9.浮躁