ASP.NET Core中的依赖注入4: 构造函数的选择与服务生命周期管理

ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationInstance和ImplementationFactory属性均为Null,那么ServiceProvider最终会利用其ImplementationType属性返回的真实类型选择一个适合的构造函数来创建最终的服务实例。我们知道服务服务的真实类型可以定义了多个构造函数,那么ServiceProvider针对构造函数的选择会采用怎样的策略呢?

目录
一、构造函数的选择
二、生命周期管理
    ServiceScope与ServiceScopeFactory
    三种生命周期管理模式
    服务实例的回收

一、构造函数的选择

如果ServiceProvider试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,最终被选择出来的构造函数必须具备一个基本的条件:ServiceProvider能够提供构造函数的所有参数。为了让读者朋友能够更加真切地理解ServiceProvider在构造函数选择过程中采用的策略,我们不让也采用实例演示的方式来进行讲解。

我们在一个控制台应用中定义了四个服务接口(IFoo、IBar、IBaz和IGux)以及实现它们的四个服务类(Foo、Bar、Baz和Gux)。如下面的代码片段所示,我们为Gux定义了三个构造函数,参数均为我们定义了服务接口类型。为了确定ServiceProvider最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。

   1: public interface IFoo {}
   2: interface IBar {}
   3: interface IBaz {}
   4: interface IGux {}
   5:  
   6: class Foo : IFoo {}
   7: class Bar : IBar {}
   8: class Baz : IBaz {}
   9: class Gux : IGux
  10: {
  11:     public Gux(IFoo foo)
  12:     {
  13:         Console.WriteLine("Gux(IFoo)");
  14:     }
  15:  
  16:     public Gux(IFoo foo,IBar bar)
  17:     {
  18:         Console.WriteLine("Gux(IFoo,IBar)");
  19:     }
  20:  
  21:       22:     {
  23:         Console.WriteLine();
  24:     }
  25: }

我们在作为程序入口的Main方法中创建一个ServiceCollection对象并在其中添加针对IFoo、IBar以及IGux这三个服务接口的服务注册,针对服务接口IBaz的注册并未被添加。我们利用由它创建的ServiceProvider来提供针对服务接口IGux的实例,究竟能否得到一个Gux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢?

2: {
   4:     {       
   6:             .AddTransient<IFoo,Foo>()
   8:             .AddTransient<IGux,Gux>()
  10:             .GetServices<IGux>();
  12: }

对于定义在Gux中的三个构造函数来说,ServiceProvider所在的ServiceCollection包含针对接口IFoo和IBar的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为IBaz的参数,这无法通过ServiceProvider来提供。根据我们上面介绍的第一个原则(ServiceProvider能够提供构造函数的所有参数),Gux的前两个构造函数会成为合法的候选构造函数,那么ServiceProvider最终会选择哪一个呢?

在所有合法的候选构造函数列表中,最终被选择出来的构造函数具有这么一个特征:每一个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。如果这样的构造函数并不存在,一个类型为InvalidOperationException的异常会被跑出来。根据这个原则,Gux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数仅仅具有一个类型为IFoo的参数,最终被选择出来的会是Gux的第二个构造函数,所有运行我们的实例程序将会在控制台上产生如下的输出结果。

   8:             .AddTransient<IBaz,Baz>()
  11:             .GetServices<IGux>();
  13: }
  15:   16: {
  18:     public Gux(IBar bar,IBaz baz) {}
   1: Unhandled Exception: System.InvalidOperationException: Unable to activate type 'Gux'. The following constructors are ambigious:
   3: Void .ctor(IBar,IBaz)
interface IServiceScope : IDisposable
   4: }
   7: {
   9: }

若要充分理解ServiceScope和ServiceProvider之间的关系,我们需要简单了解一下ServiceProvider的层级结构。除了直接通过一个ServiceCollection对象创建一个独立的ServiceProvider对象之外,一个ServiceProvider还可以根据另一个ServiceProvider对象来创建,如果采用后一种创建方式,我们指定的ServiceProvider与创建的ServiceProvider将成为一种“父子”关系。

private readonly ServiceProvider _root;
   5:     {
   7:     }
   9: }

3-11

虽然在ServiceProvider在创建过程中体现了ServiceProvider之间存在着一种树形化的层级结构,但是ServiceProvider对象本身并没有一个指向“父亲”的引用,它仅仅会保留针对根节点的引用。如上面的代码片段所示,针对根节点的引用体现为ServiceProvider类的字段_root。当我们根据作为“父亲”的ServiceProvider创建一个新的ServiceProvider的时候,父子均指向同一个“根”。我们可以将创建过程中体现的层级化关系称为“逻辑关系”,而将ServiceProvider对象自身的引用关系称为“物理关系”,右图清楚地揭示了这两种关系之间的转化。

由于ServiceProvider自身是一个内部类型,我们不能采用调用构造函数的方式根据一个作为“父亲”的ServiceProvider创建另一个作为“儿子”的ServiceProvider,但是这个目的可以间接地通过创建ServiceScope的方式来完成。如下面的代码片段所示,我们首先创建一个独立的ServiceProvider并调用其GetService<T>方法获得一个ServiceScopeFactory对象,然后调用后者的CreateScope方法创建一个新的ServiceScope,它的ServiceProvider就是前者的“儿子”。

5:         IServiceProvider serviceProvider1 = new ServiceCollection().BuildServiceProvider();
   7:  
   9:         Debug.Assert(object.ReferenceEquals(serviceProvider1,root));        
  11: }

如果读者朋友们希望进一步了解ServiceScope的创建以及它和ServiceProvider之间的关系,我们不妨先来看看作为IServiceScope接口默认实现的内部类型ServiceScope的定义。如下面的代码片段所示,ServiceScope仅仅是对一个ServiceProvider对象的简单封装而已。值得一提的是,当ServiceScope的Dispose方法被调用的时候,这个被封装的ServiceProvider的同名方法同时被执行。

2:     readonly ServiceProvider _scopedProvider;
this._scopedProvider = scopedProvider;
void Dispose()
  10:         _scopedProvider.Dispose();
  13:     public IServiceProvider ServiceProvider
  15:         get {return _scopedProvider; }
  17: }

IServiceScopeFactory接口的默认实现类型是一个名为ServiceScopeFactory的内部类型。如下面的代码片段所示,ServiceScopeFactory的只读字段“_provider”表示当前的ServiceProvider。当CreateScope方法被调用的时候,这个ServiceProvider的“子ServiceProvider”被创建出来,并被封装成返回的ServiceScope对象。

readonly ServiceProvider _provider;
   6:         _provider = provider;
   9:     public IServiceScope CreateScope()
  11:         return new ServiceScope(new ServiceProvider(_provider));
   4:  
   5:         IServiceProvider root =    7:             .AddScoped<IBar,1)">   8:             .AddSingleton<IBaz,1)">   9:             .BuildServiceProvider();
  11:         IServiceProvider child2 = root.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
  14:         Console.WriteLine("ReferenceEquals(child1.GetService<IBar>(),child1.GetService<IBar>() = {0}",ReferenceEquals(child1.GetService<IBar>(),child1.GetService<IBar>()));
  16:         Console.WriteLine("ReferenceEquals(child1.GetService<IBaz>(),child2.GetService<IBaz>() = {0}",ReferenceEquals(child1.GetService<IBaz>(),child2.GetService<IBaz>()));
  18: }

为了验证ServiceProvider针对Transient模式是否总是创建新的服务实例,我们利用同一个ServiceProvider(root)获取针对服务接口IFoo的实例并进行比较。为了验证ServiceProvider针对Scope模式是否仅仅在当前ServiceScope下具有“单例”的特性,我们先后比较了同一个ServiceProvider(child1)和不同ServiceProvider(child1和child2)两次针对服务接口IBar获取的实例。为了验证具有“同根”的所有ServiceProvider针对Singleton模式总是返回同一个服务实例,我们比较了两个不同child1和child2两次针对服务接口IBaz获取的服务实例。如下所示的输出结构印证了我们上面的论述。

2: ReferenceEquals(child1.GetService<IBar>(),child1.GetService<IBar>()     = True
   4: ReferenceEquals(child1.GetService<IBaz>(),child2.GetService<IBaz>()     = True

服务实例的回收

ServiceProvider除了为我们提供所需的服务实例之外,对于由它提供的服务实例,它还肩负起回收之责。这里所说的回收与.NET自身的垃圾回收机制无关,仅仅针对于自身类型实现了IDisposable接口的服务实例,所谓的回收仅仅体现为调用它们的Dispose方法。ServiceProvider针对服务实例所采用的收受策略取决于服务注册时采用的生命周期管理模式,具体采用的服务回收策略主要体现为如下两点:

  • 如果注册的服务采用Singleton模式,由某个ServiceProvider提供的服务实例的回收工作由作为根的ServiceProvider负责,后者的Dispose方法被调用的时候,这些服务实例的Dispose方法会自动执行。
  • 如果注册的服务采用其他模式(Scope或者Transient),ServiceProvider自行承担由它提供的服务实例的回收工作,当它的Dispose方法被调用的时候,这些服务实例的Dispose方法会自动执行。

我们照例使用一个简单的实例来演示ServiceProvider针对不同生命周期管理模式所采用的服务回收策略。我们在一个控制台应用中定义了如下三个服务接口(IFoo、IBar和IBaz)以及三个实现它们的服务类(Foo、Bar和Baz),这些类型具有相同的基类Disposable。Disposable实现了IDisposable接口,我们在Dispose方法中输出相应的文字以确定对象回收的时机。

class Bar : Disposable,IBar {}
class Disposable : IDisposable
  15: }

我们在作为程序入口的Main方法中创建了一个ServiceCollection对象,并在其中采用不同的生命周期管理模式注册了三个相应的服务(IFoo/Foo、IBar/Bar和IBaz/Baz分别采用Transient、Scoped和Singleton模式)。我们针对这个ServiceCollection创建了一个ServiceProvider(root),以及它的两个“儿子”(child1和child2)。在分别通过child1和child2提供了两个服务实例(child1:IFoo, child2:IBar/IBaz)之后,我们先后调用三个ServiceProvider(child1=>child2=>root)的Dispose方法。

14:         child1.GetService<IFoo>();
  16:         child2.GetService<IBaz>();
"child1.Dispose()");
  21:         Console.WriteLine("child2.Dispose()");
  23:  
  25:         ((IDisposable)root).Dispose();
  26:     }
  27: }

该程序运行之后会在控制台上产生如下的输出结果。从这个结果我们不难看出由child1提供的两个采用Transient模式的服务实例的回收实在child1的Dispose方法执行之后自动完成的。当child2的Dispose方法被调用的时候,对于由它提供的两个服务对象来说,只有注册时采用Scope模式的Bar对象被自动回收了,至于采用Singleton模式的Baz对象的回收工作,是在root的Dispose方法被调用之后自动完成的。

2: Foo.Dispose()
   4: child2.Dispose()
   6: root.Dispose()
void DoWork(IServiceProvider serviceProvider)
   5:         ...
   3:     IFoobar foobar = serviceProvider.GetService<IFoobar>();
   6:         ...
  10:         (foobar as IDisposable)?.Dispose();
using (IServiceScope serviceScope = serviceProvider.GetService<IServiceScopeFactory>().CreateScope())
   8: }

接下来我们通过一个简单的实例演示上述这两种针对服务回收的编程方式之间的差异。我们在一个控制台应用中定义了一个继承自IDisposable的服务接口IFoobar和实现它的服务类Foobar。如下面的代码片段所示,为了确认对象真正被GC回收的时机,我们为Foobar定义了一个析构函数。在该析构函数和Dispose方法中,我们还会在控制台上输出相应的指导性文字。

2: {}
class Foobar : IFoobar
   6:     ~Foobar()
   8:         Console.WriteLine("Foobar.Finalize()");
  10:  
  15: }

在作为程序入口的Main方法中,我们创建了一个ServiceCollection对象并采用Transient模式将IFoobbar/Foobar注册其中。借助于通过该ServiceCollection创建的ServiceProvider,我们分别采用上述的两种方式获取服务实例并试图对它实施回收。为了强制GC试试垃圾回收,我们显式调用了GC的Collect方法。

7:             .BuildServiceProvider();
  10:         GC.Collect();
  12:         Console.WriteLine("----------------");
  15:             serviceScope.ServiceProvider.GetService<IFoobar>();
  17:         GC.Collect();
  19:         Console.Read();
  21: }

该程序执行之后会在控制台上产生如下所示的输出结果。从这个结果我们可以看出,如果我们使用现有的ServiceProvider来提供所需的服务实例,后者在GC进行垃圾回收之前并不会从内存中释放。如果我们利用现有的ServiceProvider创建一个ServiceScope,并利用它所在的ServiceProvider来提供我们所需的服务实例,GC是可以将其从内存中释放出来的。

2: ----------------
   4: Foobar.Finalize()

[1] 对于分别采用 Scoped和Singleton模式提供的服务实例,当前ServiceProvider和根ServiceProvider分别具有对它们的引用。如果采用Transient模式,只有服务类型实现了IDisposable接口,当前ServiceProvider才需要对它保持引用以完成对它们的回收,否则没有任何一个ServiceProvider保持对它们的引用。

ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core中的依赖注入(2):依赖注入(DI)
ASP.NET Core中的依赖注入(3):服务注册与提取
ASP.NET Core中的依赖注入(4):构造函数的选择与生命周期管理
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【总体设计】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【解读ServiceCallSite】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便