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

时间:2022-04-05 21:37:10

我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点。由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内容相对分散和零碎,我们有必要针对这个主题作一个归纳性的介绍。采用依赖注入的服务均由某个ServiceProvider来提供,但是在ASP.NET Core管道涉及到两个不同的ServiceProvider,其中一个是在管道成功构建后创建并绑定到WebHost上的ServiceProvider,对应着WebHost的Services属性。另一个ServiceProvider则是在管道处理每个请求时即时创建的,它绑定当表示当前请求上下文上,对应着HttpContext的RequestServices属性,两个ServiceProvider之间存在着父子关系。[本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、WebHost的ServiceProvider
二、HttpContext的ServiceProvider
    原理分析
    实例证明
    两个ServiceProvider具有“父子”关系
    ServiceProvidersFeature特性
    RequestServicesContainerMiddleware中间件
    AutoRequestServicesStartupFilter

一、WebHost的ServiceProvider

ASP.NET Core的依赖注入框架其实很简单,其中仅仅涉及ServiceCollection和ServiceProvider这两个核心对象。我们预先将服务描述信息注册到ServiceCollection之上,然后利用ServiceCollection来创建ServiceProvider,并最终利用后者根据指定的服务类型来提供对应的服务实例。接下来我们以这两个对象作为唯一的关注点来回顾一下管道的创建流程。ASP.NET Core管道的创建也仅仅涉及到两个核心对象,作为应用宿主的WebHost对象和创建它的WebHostBuilder。下图基本揭示了WebHostBuilder创建WebHost,以及WebHost在开启过程针对依赖注入这两个核心对象的使用。

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

ASP.NET Core管道在构建过程中会使用同一个ServiceCollection,所有注册的服务都被添加到这个对象上。这个ServiceCollection对象最初由WebHostBuilder创建。在WebHost的创建过程中,WebHostBuilder需要向这个ServiceCollection对象注册两种类型的服务:一种是确保管道能够被成功构建并顺利处理请求所必需的服务,我们不妨将它们称为系统服务;另一种则是用户通过调用ConfigureServices方法自行注册的服务,我们姑且称它们为用户服务。

当上述这两种服务被成功注册之后,WebHostBuilder会利用这个ServiceCollection创建一个ServiceProvider对象,这个对象和ServiceCollection将一并递交给由它创建的WebHost对象。当WebHost在初始化过程中,它的第一项过程就是利用ServiceProvider获取一个Startup对象。如果这一个ConventionBasedStartup对象是,并且对应的启动类是一个实例类,具体的启动对象是采用依赖注入的形式被实例化的,所以启动类的构造函数是可以有参数的。启动对象实例化过程中使用的就是WebHostBuilder提供的这个ServiceProvider,这也是依赖注入的第一次应用。

当WebHost利用WebHostBuilder提供的这个ServiceProvider得到这个Startup对象之后,它会调用其ConfigureServices方法将用户在启动类中注册的服务添加到上述这个ServiceCollection对象之上,到目前为止这个ServiceCollection包含了所有需要注册的服务。如果启动类型的ConfigureServices方法没有返回值,那么这个ServiceCollection将被用来创建一个新的ServiceProvider,后续过程中所有的服务都会利用它来获取。如果启动类型的ConfigureServices方法返回一个ServiceProvider,那么后续过程作为服务提供者的就是这么一个对象。WebHost的Services属性返回的就是这个ServiceProvider对象,所以姑且称它为WebHost的ServiceProvider。

接下来WebHost利用这个ServiceProvider获取注册的ApplicationBuilder对象和StartupFilter对象,并将前者作为参数依次调用每个StartupFilter的Configure方法进行中间件的注册。当针对所有StartupFilter的调用都结束之后,WebHost才会选择调用Startup对象的Configure方法。对于通过这两种形式注册的中间件,如果对应的是一个遵循约定的中间件类型的话,WebHost同样会采用依赖注入的方式来实例化中间件对象,所以中间件类型的构造函数也是可以有参数的,这是对依赖注入的第二次应用。

到所有中间件都被注册之后,WebHost会调用ApplicationBuilder的Build方法生成一个RequestDelegate对象,这个对象体现了所有中间件组成一个有序链表。接下来,WebHost利用这个RequestDelegate对象创建一个HttpApplication对象(默认创建的是一个HostingHttpApplication对象)。随后,WebHost利用ServiceProvider提取出最初注册在WebHostBuilder上的服务器,并将HttpApplication对象作为参数调用其Start方法启动该服务器。从此,这个以服务器和注册中间件构成的管道被成功创建出来,服务器随之开始绑定到指定的监听地址监听来自网络的请求。

二、HttpContext的ServiceProvider

请求一旦抵达并被服务器接收,服务器会将它将给后边的中间件执行。如果中间件对应的是一个按照约定对应的中间件类型,对请求的处理体现在对它的Invoke方法的执行。针对中间件类型Invoke方法的执行同样采用了依赖注入的形式来提供该方法从第二开始的所有参数,这是对依赖注入的第三次应用。那么现在问题来了,针对每次请求所使用的ServiceProvider依然是WebHost的ServiceProvider吗?如果不是 ,那么两者是什么关系?

原理分析

我们先来回答第一个问题。对于某个由ServiceProvider提供的服务对象说,针对它的回收也是由这个ServiceProvider来完成的。具体来说,非根ServiceProvider在自身被回收的时候,由它提供的采用Scoped和Transient模式的服务实例会自动被回收;至于采用Singleton模式的服务实例,针对它们的回收发生在跟ServiceProvider自身被回收的时候。

如果我们在这个ServiceProvider上以Transient模式注册了一个服务,这意味着每次从ServiceProvider提取的都是一个全新的对象。如果这些对象引用着一些需要被回收的资源,我们希望资源的回收应该在每次请求处理结束之后自动执行。如果管道每次处理请求时所使用的都是同一个ServiceProvider对象,那么针对服务实例的回收只能在整个应用终止的时候才会发生,这无疑会产生内存泄漏的问题。基于这个原因。管道总是会创建一个新的ServiceProvider来提供处理每个请求所需的服务,并且这个ServiceProvider将在每次请求处理完成之后被自动回收掉。这样一个ServiceProvider被创建之后直接保存到当前的HTTP上下文中,我们可以利用HttpContext如下所示的RequestServices属性得到这个ServiceProvider。

   1: public abstract class HttpContext

   2: {

   3:     public abstract IServiceProvider RequestServices { get; set; }

   4:    ...

   5: }

实例证明

我们上面仅仅从理论层面解释了为什么针对每次请求所使用的ServiceProvider都不相同,接下来我们可以通过实例演示的方式来证实这个推论是成立的。我们在一个控制台应用中编写了如下的代码来启动一个ASP.NET Core应用。我们以不同的生命周期模式(Singleton、Scoped和Transient)之注册三个服务,具体的服务类型都实现了IDisposable接口,而实现的Dispose方法会在控制台上打印相应的文字指示那个类型的Dispose方法被执行了。通过调用Configure方法注册的中间件会利用从当前HttpContext获取的ServiceProvider来提供三个对象的服务对象。

   1: public class Program

   2: {

   3:     public static void Main()

   4:     {

   5:         new WebHostBuilder()

   6:             .ConfigureLogging(loggerFactory=>loggerFactory.AddConsole())

   7:             .UseKestrel()

   8:             .ConfigureServices(svcs=>svcs

   9:                 .AddSingleton<IFoo, Foo>()

  10:                 .AddScoped<IBar, Bar>()

  11:                 .AddTransient<IBaz, Baz>())

  12:             .Configure(app => app.Run(async context =>{

  13:                 context.RequestServices.GetService<IFoo>();

  14:                 context.RequestServices.GetService<IBar>();

  15:                 context.RequestServices.GetService<IBaz>();

  16:                 await context.Response.WriteAsync("End");

  17:             }))

  18:             .Build()

  19:             .Run();

  20:     } 

  21: }

  22:  

  23: public interface IFoo {}

  24: public interface IBar {}

  25: public interface IBaz {}

  26: public class ServiceBase : IDisposable

  27: {

  28:     public void Dispose()

  29:     {

  30:         Console.WriteLine($"{this.GetType().Name}.Dispose()...");

  31:     }

  32: }

  33: public class Foo : ServiceBase, IFoo {}

  34: public class Bar : ServiceBase, IBar {}

  35: public class Baz : ServiceBase, IBaz {}

由于我们调用 WebHostBuilder的ConfigureLogging方法添加了ConsoleLoggerProvider,所以管道在开始和结束请求的时候会在当前控制台上写入相应的日志。启动应用之后,我们利用浏览器向默认的监听地址连续发送两次请求后,控制台上将会产生如下所示的输出结果。这样的输出结果表明:对于当前请求处理过程中获取的非Sington服务对象都会请求处理结束之后被自动回收。

   1: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]

   2:       Request starting HTTP/1.1 GET http://localhost:5000/

   3: Baz.Dispose()...

   4: Bar.Dispose()...

   5: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]

   6:       Request finished in 74.9439ms 200

   7:  

   8: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]

   9:       Request starting HTTP/1.1 GET http://localhost:5000/

  10: Baz.Dispose()...

  11: Bar.Dispose()...

  12: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]

  13:       Request finished in 0.8272ms 200

两个ServiceProvider具有“父子”关系

回到前面提到的第二个问题,处理每个请求创建的ServiceProvider和管道构建成功时创建的ServiceProvider(对应WebHost的Services属性)之间具有怎样的关系,其实两者之间的关系很简单,是“父子”关系。下图不仅仅体现了这两种类型的ServiceProvider各自具有的生命周期,同时也体现了它们之间的关系。WebHost的生命周期也就是整个应用的生命周期,所以WebHost的Services属性返回的ServiceProvider是一个全局单例对象。当WebHost随着其Dispose方法被调用而被关闭时,它会调用ServiceProvider的Dispose方法。

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

ASP.NET Core管道针对每个请求的处理都在一个全新的HTTP上下文(HttpContext)中进行,提供请求处理所需服务的ServiceProvider与当前上下文绑定在一起,通过HttpContext对象的RequestServices属性返回。由于这个ServiceProvider将WebHost的ServiceProvider作为“父亲” ,所以之前添加的所有服务注册对于它来说依然有效。当前请求一旦结束,当前HttpContext自然 “寿终正寝” ,与之关联的ServiceProvider也随之被回收释放。

ServiceProvidersFeature特性

在了解了两种类型的ServiceProvider各种具有的生命周期和相互关系之后,我们需要了解这个为请求处理提供服务的ServiceProvider是如何被创建,又是如何被回收释放的。对作为默认HttpContext的DefaultHttpContext对象来说,它的RequestServices属性返回的ServiceProvider来源于一个名为ServiceProvidersFeature的特性。所谓的ServiceProvidersFeature特性是对所有实现了IServiceProvidersFeature接口的类型以及对应对象的统称。如下面的代码片段所示,这个接口具有一个唯一属性RequestServices正好用于返回和设置这个ServiceProvider。

   1: public interface IServiceProvidersFeature

   2: {

   3:     IServiceProvider RequestServices { get; set; }

   4: }

ASP.NET Core默认使用的ServiceProvidersFeature是一个类型为RequestServicesFeature的对象,如下所示的代码片段体现了它提供ServiceProvider的逻辑。在创建一个RequestServicesFeature对象的时候,我们需要提供一个根据某个ServiceProvider创建 ServiceScopeFactory对象,它所提供的ServiceProvider就是根据这个ServiceScopeFactory提供的ServiceScope对象创建的。我们根据根据提供的代码可知针对这个属性的多次调用返回的实际上是同一个ServiceProvider。RequestServicesFeature还是实现IDisposable接口,并在实现的Dispose放过中释放了这个ServiceScope,我们知道此举实际上是为了实现对提供的这个ServiceProvider实施回收。

   1: public class RequestServicesFeature : IServiceProvidersFeature, IDisposable

   2: {

   3:     private IServiceScopeFactory     _scopeFactory;

   4:     private IServiceProvider         _requestServices;

   5:     private IServiceScope            _scope;

   6:     private bool                     _requestServicesSet;

   7:  

   8:     public RequestServicesFeature(IServiceScopeFactory scopeFactory)

   9:     {

  10:         _scopeFactory = scopeFactory;

  11:     }

  12:  

  13:     public IServiceProvider RequestServices

  14:     {

  15:         get

  16:         {

  17:             if (!_requestServicesSet)

  18:             {

  19:                 _scope = _scopeFactory.CreateScope();

  20:                 _requestServices = _scope.ServiceProvider;

  21:                 _requestServicesSet = true;

  22:             }

  23:             return _requestServices;

  24:         }

  25:  

  26:         set

  27:         {

  28:             _requestServices = value;

  29:             _requestServicesSet = true;

  30:         }

  31:     }

  32:  

  33:     public void Dispose()

  34:     {

  35:         _scope?.Dispose();

  36:         _scope = null;

  37:         _requestServices = null;

  38:     }

  39: }

RequestServicesContainerMiddleware中间件

那么这个RequestServicesFeature特性又是如何被添加到当前HttpContext的特性集合中的呢?这实际上又涉及到一个名为RequestServicesContainerMiddleware的中间件。我们在创建这个中间件的时候需要提供一个ServiceScopeFactory,该中间件会在Invoke方法被执行的时候根据它创建一个RequestServicesFeature对象,并将其添加到当前HttpContext的特性集合中。当后续的请求处理结束之后,添加的这个RequestServicesFeature对象会被回收释放,并从HttpContext的特性集合中去除。实际上HttpContext的RequestServices返回的ServiceProvider就是在这里被回收释放的。

   1: public class RequestServicesContainerMiddleware

   2: {

   3:     private readonly RequestDelegate     _next;

   4:     private IServiceScopeFactory         _scopeFactory;

   5:  

   6:     public RequestServicesContainerMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory)

   7:     {        

   8:         _scopeFactory     = scopeFactory;

   9:         _next             = next;

  10:     }

  11:  

  12:     public async Task Invoke(HttpContext httpContext)

  13:     {           

  14:  

  15:         var existingFeature = httpContext.Features.Get<IServiceProvidersFeature>();

  16:         if (existingFeature?.RequestServices != null)

  17:         {

  18:             await _next.Invoke(httpContext);

  19:             return;

  20:         }

  21:  

  22:         using (var feature = new RequestServicesFeature(_scopeFactory))

  23:         {

  24:             try

  25:             {

  26:                 httpContext.Features.Set<IServiceProvidersFeature>(feature);

  27:                 await _next.Invoke(httpContext);

  28:             }

  29:             finally

  30:             {

  31:                 httpContext.Features.Set(existingFeature);

  32:             }

  33:         }

  34:     }

  35: }

AutoRequestServicesStartupFilter

RequestServicesContainerMiddleware中间件的注册最终通过一个StartupFilter对象来完成的,它的类型就是具有如下定义的AutoRequestServicesStartupFilter。对于其Configure方法返回的这个Action<IApplicationBuilder>对象来说,它在注册这个中间件的时候并没有明确之定义一个具体的ServiceScopeFactory对象,那么毫无疑问该中间件使用的ServiceScopeFactory就是根据WebHost的ServiceProvider提供的。WebHost的ServiceProvider提供了一个ServiceScopeFactory,而HttpContext的ServiceProvider又是根据这个ServiceScopeFactory提供的ServiceScope创建的,这两个ServiceProvider之间的父子关系就是采用形式确立的。

   1: public class AutoRequestServicesStartupFilter : IStartupFilter

   2: {

   3:     public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)

   4:     {

   5:         return app =>

   6:         {

   7:             app.UseMiddleware<RequestServicesContainerMiddleware>();

   8:             next(app);

   9:         };

  10:     }

  11: }

在WebHostBuilder创建WebHost之前,它会注册一系列确保后续的管道能够正常构建并处理请求所必须的服务,这其中就包括这个AutoRequestServicesStartupFilter。综上所述,通过HttpContext的RequestServices属性返回的一个用于提供请求处理过程所需服务的ServiceProvider,这个ServiceProvider的创建和回收释放按是通过一个特性(RequestServicesFeature)、一个中间件(RequestServicesContainerMiddleware)和一个StartupFilter(AutoRequestServicesStartupFilter)相互协作完成的。

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

我们知道注册服务具有三种生命周期模式(Singleton、Scoped和Transient)。由于为请求处理提供所需服务的ServiceProvider是基于当前请求上下文的,所以这三种生命周期模式在ASP.NET Core应用中体现了服务实例的复用等级。具体来说,Singleton服务在整个应用生命周期中复用,Scoped服务仅在当前请求上下文中复用,而Transient服务则不能被复用,