4.3 服务的消费
包含服务注册信息的 IServiceCollection 集合最终被用来创建作为依赖注入容器的IServiceProvider 对象。当需要消费某个服务实例的时候,只需要指定服务类型调用IServiceProvider 接口的 GetService 方法即可,IServiceProvider 对象就会根据对应的服务注册提供所需的服务实例。
4.3.1 IServiceProvider
如下面的代码片段所示,IServiceProvider 接口定义了唯一的 GetService 方法,可以根据指定的类型来提供对应的服务实例。当利用包含服务注册的 IServiceCollection 对象创建出IServiceProvider 对象之后,我们需要将服务注册的服务类型(对应 ServiceDescriptor 的ServiceType 属性)作为参数调用 GetService 方法,该方法能够根据服务注册信息提供对应的服务实例。
针对IServiceProvider对象的创建,体现在IServiceCollection接口的3个BuildServiceProvider扩展方法重载上。如下面的代码片段所示,这 3 个扩展方法提供的都是一个类型为ServiceProvider 的对象,该对象是根据提供的配置选项创建的。配置选项类型ServiceProviderOptions提供了两个属性:ValidateScopes属性表示是否需要开启针对服务范围的检验;ValidateOnBuild 属性表示是否需要预先检验作为服务注册的每个 ServiceDescriptor 对象能否提供对应的服务实例。在默认情况下,ValidateScopes 和 ValidateOnBuild 描述的检验都是关闭的。
虽然调用 IServiceCollection 接口的 BuildServiceProvider 扩展方法返回的总是一个ServiceProvider对象,但是笔者并不打算详细介绍这个类型,这是因为实现在该类型中针对服务实例的提供机制一直在不断变化,而且这个变化趋势在未来版本更替过程中可能还将继续下去。除此之外,ServiceProvider对象还涉及一系列内部类型和接口,所以本书不涉及具体的细节,只进行总体设计。
除了定义在 IServiceProvider接口中的 GetService方法,该接口提供服务实例的扩展方法还有如下几种:GetService<T>方法以泛型参数的形式指定了服务类型,返回的服务实例也会做对应的类型转换。如果指定服务类型的服务注册不存在,GetService 方法会返回 Null,如果调用GetRequiredService方法或者 GetRequiredService<T>方法则会抛出一个 InvalidOperationException类型的异常。如果所需的服务实例是必需的,我们一般会调用这两个GetRequiredService扩展方法。
所以,如果针对某个类型添加了多个服务注册,那么 GetService 方法总是采用最新添加的服务注册来提供服务实例。如果希望利用所有的服务注册创建一组服务实例列表,我们既可以调用 GetServices 方法或者 GetServices<T>方法,也可以调用 GetService<IEnumerable<T>>方法。
4.3.2 服务实例的创建
对于通过调用IServiceCollection集合的BuildServiceProvider方法创建的IServiceProvider对象来说,如果通过指定服务类型调用其 GetService 方法以获取对应的服务实例,那么它总是会根据提供的服务类型从服务注册列表中找到对应的ServiceDescriptor对象,并根据它来提供所需的服务实例。
ServiceDescriptor对象具有3个不同的构造函数,分别对应服务实例最初的3种提供方式,我们既可以提供一个 Func<IServiceProvider,object>对象作为工厂来创建对应的服务实例,也可以直接提供一个创建好的服务实例。如果提供的是服务的实现类型,最终提供的服务实例将通过调用该类型的某个构造函数来创建,那么构造函数是通过什么策略被选择出来的?
如果IServiceProvider对象试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,所以最终被选择的构造函数必须具备一个基本的条件,即IServiceProvider对象能够提供构造函数的所有参数。为了使读者能够更加深刻地理解IServiceProvider对象在构造函数选择过程中采用的策略,我们会采用实例演示的方式对此进行讲述。
我们在一个控制台应用中定义了 4个服务接口(IFoo、IBar、IBaz和 IQux)以及实现它们的4个类(Foo、Bar、Baz和Qux)。如下面的代码片段所示,我们为Qux定义了3个构造函数,参数都定义了服务接口类型。为了确定 IServiceProvider 最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。
如下演示程序创建了一个ServiceCollection对象,并在其中添加针对IFoo、IBar及IQux这3个服务接口的服务注册,但未添加针对服务接口IBaz的注册。利用由这个ServiceCollection对象创建的 IServiceProvider来提供针对服务接口 IQux的实例,是否能够得到一个 Qux对象?如果可以,它又是通过执行哪个构造函数创建的?
对于定义在 Qux 中的 3 个构造函数来说,由于创建 IServiceProvider 对象提供的 IService Collection 集合包含针对 IFoo 接口和 IBar 接口的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为 IBaz 的参数,所以无法通过IServiceProvider 对象来提供。根据前面介绍的第一个原则(IServiceProvider 对象能够提供构造函数的所有参数),Qux 的前两个构造函数会成为合法的候选构造函数,那么IServiceProvider对象最终会选择哪一个构造函数?
在所有合法的候选构造函数列表中,最终被选择的构造函数具有如下特征:每个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。如果这样的构造函数并不存在,一个InvalidOperationException类型的异常会被抛出来。根据这个原则,Qux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数只具有一个类型为IFoo的参数,所以最终被选择的是Qux的第二个构造函数,运行实例程序,控制台上产生的输出结果如图4-6所示。(S408)
图4-6 构造函数的选择策略
下面对实例程序略加改动。如下面的代码片段所示,我们只为 Qux 定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo&IBar和IBar&IBaz,并且将针对IBaz/Baz的服务注册添加到创建的ServiceCollection集合中。
虽然Qux的两个构造函数的参数都可以由IServiceProvider对象来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以 IServiceProvider 对象无法选择一个最佳的构造函数。运行该程序后会抛出图 4-7所示的 InvalidOperationException类型的异常,并提示无法从两个候选的构造函数中选择一个最优的来创建服务实例。(S409)
图4-7 构造函数的选择策略
4.3.3 生命周期
生命周期决定了 IServiceProvider 对象采用什么样的方式提供和释放服务实例。虽然不同版本的依赖注入框架针对服务实例的生命周期管理采用了不同的实现方式,但总的来说原理还是类似的。在提供的依赖注入框架 Cat 中,我们已经模拟了 3 种生命周期模式的实现原理,下面结合服务范围做进一步阐述。
服务范围
对于依赖注入框架采用的 3 种生命周期模式(Singleton、Scoped 和 Transient)来说,Singleton和Transient都具有明确的语义,但是很多初学者不清楚Scoped代表一种什么样的生命周期模式。Scoped指的是由IServiceScope接口表示的服务范围,该范围由IServiceScopeFactory接口表示的“服务范围工厂”来创建。如下面的代码片段所示,IServiceProvider的 CreateScope扩展方法正是利用提供的IServiceScopeFactory服务实例来创建作为服务范围的IServiceScope对象的。
任何一个IServiceProvider对象都可以利用其注册的IServiceScopeFactory服务创建一个代表服务范围的IServiceScope对象,后者代表的“范围”内具有一个新创建的IServiceProvider对象(对应 IServiceScope接口的 ServiceProvider属性),该对象与当前 IServiceProvider在逻辑上具有图4-8所示的“父子关系”。
图4-8 IServiceScope与IServiceProvider(逻辑结构)
图 4-8 所示的树形层次结构只是一种逻辑结构,从对象引用层面来看,通过某个 IService Scope 封装的 IServiceProvider 对象不需要知道自己的“父亲”是谁,它只关心作为根节点的IServiceProvider对象在哪里。图 4-9从物理层面揭示了 IServiceScope/IServiceProvider对象之间的关系,任何一个IServiceProvider对象都具有针对根容器的引用。
图4-9 IServiceScope与IServiceProvider(物理结构)
3种生命周期模式
只有充分了解IServiceScope对象的创建过程以及它与IServiceProvider对象之间的关系,我们才会对 3 种生命周期管理模式(Singleton、Scoped 和 Transient)具有深刻的认识。就服务实例的提供方式来说,它们之间具有如下几方面差异。
● Singleton:IServiceProvider对象创建的服务实例保存在作为根容器的 IServiceProvider对象中,所以多个同根的IServiceProvider对象提供的针对同一类型的服务实例都是同一个对象。
● Scoped:IServiceProvider对象创建的服务实例由自己保存,所以同一个 IServiceProvider对象提供的针对同一类型的服务实例均是同一个对象。
● Transient:针对每次服务提供请求,IServiceProvider 对象总是创建一个新的服务实例。
IServiceProvider 除了提供所需的服务实例,对于由它提供的服务实例,它还具有回收释放的作用。这里所说的回收释放与.NET Core 自身的垃圾回收机制无关,仅仅针对自身类型实现了IDisposable接口或者IAsyncDisposable接口的服务实例(下面称为Disposable服务实例),针对服务实例的释放体现为调用它们的Dispose方法或者DisposeAsync方法。IServiceProvider对象针对服务实例采用的回收释放策略取决于采用的生命周期模式,具体策略主要体现为如下两点。
● Singleton:提供 Disposable服务实例保存在作为根容器的 IServiceProvider对象上,只有在这个IServiceProvider对象被释放的时候,这些Disposable服务实例才能被释放。
● Scoped和 Transient:IServiceProvider对象会保存由它提供的Disposable服务实例,当自己被释放的时候,这些Disposable服务实例就会被释放。
综上所述,每个作为依赖注入容器的 IServiceProvider对象都具有图 4-10所示的两个列表来存放服务实例,其被分别命名为Realized Services和Disposable Services,对于一个作为非根容器的IServiceProvider对象来说,由它提供的Scoped服务保存在自身的Realized Services列表中,而Singleton 服务实例保存在根容器的 Realized Services 列表中。如果服务实现类型实现了IDisposable接口或者IAsyncDisposable接口,Scoped服务实例和Transient服务实例则保存在自身的Disposable Services列表中,而Singleton服务实例保存在根容器的Disposable Services列表中。
图4-10 生命周期管理
对于作为根容器的IServiceProvider对象来说,Singleton和Scoped对它来说是两种等效的生命周期模式,由它提供的Singleton服务实例和Scoped服务实例保存在自身的Realized Services列表中,而所有需要被释放的服务实例保存在 Disposable Services 列表中。当某个IServiceProvider 对象被用于提供针对指定类型的服务实例时,它会根据服务类型提取出表示服务注册的ServiceDescriptor对象,并根据它得到对应的生命周期模式。
● 如果提供服务的生命周期模式为Singleton,并且作为根容器的Realized Services列表中包含对应的服务实例,那么它将作为最终提供的服务实例。如果这样的服务实例尚未创建,那么新的服务将会被创建出来并作为提供的服务实例。这个服务实例会被添加到根容器的Realized Services列表中。如果实例类型实现了IDisposable接口或者IAsyncDisposable接口,创建的服务实例就会被添加到根容器的Disposable Services列表中。
● 如果提供服务的生命周期模式为Scoped,那么IServiceProvider会先确定自身的Realized Services列表中是否存在对应的服务实例。如果Realized Services列表中存在对应的服务实例,那么该服务实例将作为最终的返回值;如果 Realized Services 列表中不存在对应的服务实例,那么新的服务实例会被创建出来。在作为最终的服务实例被返回之前,新创建的服务实例会被添加到自身的 Realized Services 列表中,如果实例类型实现了IDisposable 接口或者 IAsyncDisposable 接口,新创建的服务实例会被添加到自身的Disposable Services列表中。
● 如果提供服务的生命周期模式为Transient,那么IServiceProvider会直接创建一个新的服务实例。在作为最终的服务实例被返回之前,如果实例类型实现了IDisposable接口或者IAsyncDisposable接口,创建的服务实例会被添加到自身的Disposable Services列表中。
对于非根容器的 IServiceProvider 对象来说,它的生命周期是由“包裹”着它的IServiceScope对象控制的。从前面给出的定义可以看出,IServiceScope实现了 IDisposable接口,Dispose方法的执行不仅标志着当前服务范围的终结,也意味着对应IServiceProvider对象生命周期的结束。
当代表服务范围的 IServiceScope 对象的 Dispose 方法被调用的时候,它会调用对应IServiceProvider对象的Dispose方法。一旦IServiceProvider对象因自身Dispose方法的调用而被释放的时候,它会从自身的Disposable Services列表中提取出所有需要被释放的服务实例,并调用它们的Dispose方法或者DisposeAsync方法。在这之后,Disposable Services列表和Realized Services列表会被清空,列表中的服务实例和IServiceProvider对象自身会成为垃圾对象被GC回收。
ASP.NET Core应用
依赖注入框架所谓的服务范围在 ASP.NET Core 应用中具有明确的边界,指的是针对每个HTTP请求的上下文,也就是说,服务范围的生命周期与每个请求的上下文绑定在一起。如图4-11所示,ASP.NET Core 应用中用于提供服务实例的 IServiceProvider 对象分为两种类型:一种是作为根容器并与应用具有相同生命周期的 IServiceProvider 对象,一般称为 ApplicationServices;另一种是根据请求及时创建和释放的IServiceProvider对象,一般称为RequestServices。
在 ASP.NET Core 应用初始化过程(即请求管道构建过程)中使用的服务实例都是由ApplicationServices提供的。在具体处理每个请求时,ASP.NET Core框架会利用注册的一个中间件来针对当前请求创建一个代表服务范围的 IServiceScope对象,该服务范围提供的 RequestServices用来提供当前请求处理过程中所需的服务实例。一旦服务请求处理完成,IServiceScoped 对象代表的服务范围被终结,在当前请求处理过程中的Scoped服务会变成垃圾对象并最终被GC回收。对于实现了IDisposable接口或者IAsyncDisposable接口的Scoped服务实例或者Transient服务实例来说,在变成垃圾对象之前,它们的Dispose方法或者DisposeAsync方法会被调用。
图4-11 生命周期管理