[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期

时间:2022-06-01 12:40:19

生命周期决定了IServiceProvider对象采用怎样的方式提供和释放服务实例。虽然不同版本的依赖注入框架针对服务实例的生命周期管理采用了不同的实现,但总的来说原理还是类似的。在我们提供的依赖注入框架Cat中,我们已经模拟了三种生命周期模式的实现原理,接下来我们结合“服务范围”的概念来对这个话题做进一步讲述。

一、服务范围(Service Scope)

对于依赖注入框架采用的三种生命周期模式(Singleton、Scoped和Transient)来说,Singleton和Transient都具有明确的语义,但是Scoped代表一种怎样的生命周期模式,很多初学者往往搞不清楚。这里所谓的Scope指的是由IServiceScope接口表示的“服务范围”,该范围由IServiceScopeFactory接口表示的“服务范围工厂”来创建。如下面的代码片段所示,IServiceProvider的扩展方法CreateScope正是利用提供的IServiceScopeFactory服务实例来创建作为服务范围的IServiceScope对象。

public interface IServiceScope : IDisposable
{
IServiceProvider ServiceProvider { get; }
} public interface IServiceScopeFactory
{
IServiceScope CreateScope();
} public static class ServiceProviderServiceExtensions
{
public static IServiceScope CreateScope(this IServiceProvider provider) => provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}

任何一个IServiceProvider对象都可以利用其注册的IServiceScopeFactory服务创建一个代表服务范围的IServiceScope对象,后者代表的“范围”内具有一个新创建的IServiceProvider对象(对应着接口IServiceScope的ServiceProvider属性),该对象与当前IServiceProvider在逻辑上具有如下图所示的“父子关系”。

[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期

如上图所示的树形层次结构只是一种逻辑结构,从对象引用层面来看,通过某个IServiceScope封装的IServiceProvider对象不需要知道自己的“父亲”是谁,它只关心作为根节点的IServiceProvider在哪里就可以了。下图从物理层面揭示了IServiceScope / IServiceProvider对象之间的关系,任何一个IServiceProvider对象都具有针对根容器的引用。

[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期

二、服务实例的提供

只有在充分了解IServiceScope对象的创建过程以及它与IServiceProvider对象之间的关系之后,我们才会对三种生命周期管理模式(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对象都具有如下图所示的两个列表来存放服务实例,我们将它们分别命名为“Realized Services”和“Disposable Services”,对于一个作为非根容器的IServiceProvider对象来说,由它提供的Scoped服务保存在自身的Realized Services列表中,Singleton服务实例则会保存在根容器的Realized Services列表中。如果服务实现类型实现了IDisposable或者IAsyncDisposable接口,Scoped和Transient服务实例会被保存到自身的Disposable Services列表中,而Singleton服务实例则会保存到根容器的Disposable Services列表中。

[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期

对于作为根容器的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列表中,如果实例类型实现了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请求的上下文,也就是服务范围的生命周期与每个请求上下文绑定在一起。如下图所示,ASP.NET Core应用中用于提供服务实例的IServiceProvider对象分为两种类型,一种是作为根容器并与应用具有相同生命周期的IServiceProvider对象,另一个类则是根据请求及时创建和释放的IServiceProvider对象,我们一般将它们分别称为ApplicationServices和RequestServices。

[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期

在ASP.NET Core应用初始化过程(即请求管道构建过程)中使用的服务实例都是由ApplicationServices提供的。在具体处理每个请求时,ASP.NET Core框架会利用注册的一个中间件来针对当前请求创建一个代表服务范围的IServiceScope对象,该服务范围提供的RequestServices用来提供当前请求处理过程中所需的服务实例。一旦服务请求处理完成,IServiceScoped对象代表的服务范围被终结,在当前请求处理过程中的Scoped服务会变成垃圾对象并最终被GC回收。对于实现了IDisposable或者IAsyncDisposable接口的Scoped或者Transient服务实例来说,在变成垃圾对象之前,它们的Dispose或者DisposeAsync方法会被调用。

[ASP.NET Core 3框架揭秘] 依赖注入[1]:控制反转
[ASP.NET Core 3框架揭秘] 依赖注入[2]:IoC模式
[ASP.NET Core 3框架揭秘] 依赖注入[3]:依赖注入模式
[ASP.NET Core 3框架揭秘] 依赖注入[4]:一个迷你版DI框架
[ASP.NET Core 3框架揭秘] 依赖注入[5]:利用容器提供服务
[ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
[ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
[ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
[ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配