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

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

概述ASP.NET Core的核心是通过一个Server和若干注册的Middleware构成的管道,不论是管道自身的构建,还是Server和Middleware自身的实现,以及构建在这个管道的应用,都需要

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对象上。

public interface IWebHostBuilder
   3:     IWebHostBuilder ConfigureServIEcs(Action<IServiceCollection> configureServices);
   2: interface Ibar { }
   4: class bar : Ibar { }
   6: class Program
   7: {
   8:     static voID Main(string[] args)
   9:     {
  10:           11:             .ConfigureServices(services=>services.AddSingleton<IFoo,Foo>())
  12:             .UseKestrel()
  13:             .UseStartup<Startup>()
  14:             .Build()
  15:             .Run();
  16:     }
  17: }
  18: class Startup
  19: {
  20:     public IFoo Foo { get; private set; }
  21:     public Startup(IFoo foo)
  22:     {
  23:         this.Foo = foo;
  24:     }    
  25:     voID ConfigureServices(IServiceCollection services)
  26:     {
  27:         services.AddTransIEnt<Ibar,bar>();
  28:     }
  29:     
  30:     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来创建它,为此我们对上面这个程序进行了如下的改写。

3: 4: {
   7:             .UseStartup<Startup>()
   9:             .Run();
  11: }
  14:     public IServiceProvIDer ConfigureServices(IServiceCollection services)
  16:         IServiceCollection newServices = new ServiceCollection();
  18:         {
  20:         }
  22:         return newServices
  24:             .AddSingleton<Ibar,bar>()
  26:     }
  28:       29:     {
  31:         {
  34:             await context.Response.WriteAsync($  35:         });
  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方法中直接采用方法注入的方式来使用这些预定义的服务。

13: {
  15:         IApplicationBuilder app,
  21:     {
  23:         {
  26:             await context.Response.WriteAsync($"IHostingEnvironment=>{environment}<br/>");
  28:             await context.Response.WriteAsync($"IhttpContextFactory=>{httpContextFactory}<br/>");
  30:             await context.Response.WriteAsync($"DiagnosticListener=>{diagnosticListener}");
  32:     }
   8:                 .AddSingleton<IFoo,Foo>()
  10:                 .AddMvc())
  12:             .Build()
  14:     }
  16:  
  18: {
  24:           25:         this.bar = bar;
  28:     [httpGet("/")]
  30:     {
  33:     }       
					

保存