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