ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起
我们一致在说 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管道在构建过程中会使用同一个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管道针对每个请求的处理都在一个全新的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)相互协作完成的。
我们知道注册服务具有三种生命周期模式(Singleton、Scoped和Transient)。由于为请求处理提供所需服务的ServiceProvider是基于当前请求上下文的,所以这三种生命周期模式在ASP.NET Core应用中体现了服务实例的复用等级。具体来说,Singleton服务在整个应用生命周期中复用,Scoped服务仅在当前请求上下文中复用,而Transient服务则不能被复用,
ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起的更多相关文章
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- [ASP.NET Core 3框架揭秘] 依赖注入:控制反转
ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务
毫不夸张地说,整个ASP.NET Core框架是建立在依赖注入框架之上的.ASP.NET Core应用在启动时构建管道以及利用该管道处理每个请求过程中使用到的服务对象均来源于依赖注入容器.该依赖注入容 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
生命周期决定了IServiceProvider对象采用怎样的方式提供和释放服务实例.虽然不同版本的依赖注入框架针对服务实例的生命周期管理采用了不同的实现,但总的来说原理还是类似的.在我们提供的依赖注入 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配
.NET Core具有一个承载(Hosting)系统,承载需要在后台长时间运行的服务,一个ASP.NET Core应用仅仅是该系统承载的一种服务而已.承载系统总是采用依赖注入的方式来消费它在服务承载过 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
<服务注册>.<服务消费>和<生命周期>主要从实现原理的角度对.NET Core的依赖注入框架进行了介绍,接下来更进一步,看看该框架的总体设计和实现.在过去的多个版 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
包含服务注册信息的IServiceCollection集合最终被用来创建作为依赖注入容器的IServiceProvider对象.当需要消费某个服务实例的时候,我们只需要指定服务类型调用IService ...
- [ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
通过<利用容器提供服务>我们知道作为依赖注入容器的IServiceProvider对象是通过调用IServiceCollection接口的扩展方法BuildServiceProvider创 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[4]:一个Mini版的依赖注入框架
在前面的章节中,我们从纯理论的角度对依赖注入进行了深入论述,我们接下来会对.NET Core依赖注入框架进行单独介绍.为了让读者朋友能够更好地理解.NET Core依赖注入框架的设计与实现,我们按照类 ...
随机推荐
- Ubuntu下使用nvm
写在前面:刚写着写着博客就跨年了,希望新的一年大家万事如意,一切向"前"看! 安装 wget -qO- https://raw.githubusercontent.com/crea ...
- Taurus.MVC 2.0 开源发布:WebAPI开发教程
背景: 有用户反映,Tausus.MVC 能写WebAPI么? 能! 教程呢? 嗯,木有! 好吧,刚好2.0出来,就带上WEBAPI教程了! 开源地址: https://github.com/cyq1 ...
- 通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道是如何构建起来的?
在<中篇>中,我们对管道的构成以及它对请求的处理流程进行了详细介绍,接下来我们需要了解的是这样一个管道是如何被构建起来的.总的来说,管道由一个服务器和一个HttpApplication构成 ...
- javascript:逆波兰式表示法计算表达式结果
逆波兰式表示法,是由栈做基础的表达式,举个例子: 5 1 2 + 4 * + 3 - 等价于 5 + ((1 + 2) * 4) - 3 原理:依次将5 1 2 压入栈中, 这时遇到了运算符 + ...
- 谈谈一些有趣的CSS题目(一)-- 左边竖条的实现方法
开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...
- Asp.net Core 初探(发布和部署Linux)
前言 俗话说三天不学习,赶不上刘少奇.Asp.net Core更新这么长时间一直观望,周末帝都小雨,宅在家看了下Core Web App,顺便搭建了个HelloWorld环境来尝尝鲜,第一次看到.Ne ...
- Kooboo CMS技术文档之三:切换数据存储方式
切换数据存储方式包括以下几种: 将文本内容存储在SqlServer.MySQL.MongoDB等数据库中 将站点配置信息存储在数据库中 将后台用户信息存储在数据库中 将会员信息存储在数据库中 将图片. ...
- Android中ListView实现图文并列并且自定义分割线(完善仿微信APP)
昨天的(今天凌晨)的博文<Android中Fragment和ViewPager那点事儿>中,我们通过使用Fragment和ViewPager模仿实现了微信的布局框架.今天我们来通过使用Li ...
- Mysql 学习之基础操作
一.表复制 1.复制表结构 将表hello的结构复制一份为表hello3 2.复制数据 a.如果两张表的结构一样且你要复制所有列的数据 mysql> insert into hello3 ...
- 关系型数据库与NoSQL数据库
关系型数据库的优缺点 优点: 可以做事务处理,从而保证了数据的一致性: 可以进行JOIN等多表查询: 由于以SQL标准化为前提,数据更新的开销很小(相同的字段基本上都只有一处). 缺点: 大量数据的写 ...