目录
一、IServiceProvider
二、构造函数的选择
三、服务范围
四、三种生命周期模式
五、ASP.NET Core应用下的生命周期
六、服务范围检验
一、IServiceProvider
如下面的代码片段所示,IServiceProvider接口定义了唯一的方法GetService方法根据指定的服务类型来提供对应的服务实例。当我们在利用包含服务注册的IServiceCollection对象创建对作为DI容器的IServiceProvider对象之后,我们只需要将服务注册的服务类型(对应于ServiceDescriptor的ServiceType属性)作为参数调用GetService方法,后者就能根据服务注册信息为我们提供对应的服务实例。
public interface IServiceProvider { object GetService(Type serviceType); } public static class ServiceCollectionContainerBuilderExtensions { public static ServiceProvider BuildServiceProvider(this IServiceCollection services); }
默认情况下调用IServiceCollection的BuildServiceProvider方法返回的一个ServiceProvider对象,但是我并不打算详细介绍这个类型,这是因为实现在该类型中针对服务实例的提供机制一直在不断的变化,而且这个变化趋势在未来版本更替过程中还将继续。除此之外,ServiceProvider涉及到一系列内部类型和接口,所以我们不打算涉及具体的细节,只讲总体设计。
除了定义在IServiceProvider的这个GetService方法,DI框架为了该接口定了如下这些扩展方法。GetService<T>方法会泛型参数的形式指定了服务类型,返回的服务实例也会作对应的类型转换。如果指定服务类型的服务注册不存在,GetService方法会返回Null,如果调用GetRequiredService或者GetRequiredService<T>方法则会抛出一个InvalidOperationException类型的异常。如果所需的服务实例是必需的,我们一般会调用者两个扩展方法。
public static class ServiceProviderServiceExtensions { public static T GetService<T>(this IServiceProvider provider); public static T GetRequiredService<T>(this IServiceProvider provider); public static object GetRequiredService(this IServiceProvider provider, Type serviceType); public static IEnumerable<T> GetServices<T>(this IServiceProvider provider); public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType); }
如果针对某个类型注册了多个服务,那么GetService方法总是会采用最新添加的服务注册来提供服务实例。如果希望利用所有的服务注册来创建一组服务实例列表,我们可以调用GetServices或者GetServices<T>方法。
二、构造函数的选择
对于通过调用IServiceCollection的BuildServiceProvider方法创建的IServiceProvider来说,当我们通过指定服务类型调用其GetService方法以获取对应的服务实例的时候,它总是会根据提供的服务类型从服务注册列表中找到对应的ServiceDescriptor对象,并根据后者提供所需的服务实例。
ServiceDescriptor具有三个不同的构造函数,分别对应着服务实例最初的三种创建方式,我们可以提供一个Func<IServiceProvider, object>对象作为工厂来创建对应的服务实例,也可以直接提供一个创建好的服务实例。如果我们提供的是服务的实现类型,那么最终提供的服务实例将通过调用该类型的某个构造函数来创建,那么构造函数时通过怎样的策略被选择出来的呢?
如果IServiceProvider对象试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,最终被选择出来的构造函数必须具备一个基本的条件:IServiceProvider能够提供构造函数的所有参数。为了让读者朋友能够更加真切地理解IServiceProvider在构造函数选择过程中采用的策略,我们不让也采用实例演示的方式来进行讲解。
我们在一个控制台应用中定义了四个服务接口(IFoo、IBar、IBaz和IGux)以及实现它们的四个服务类(Foo、Bar、Baz和Gux)。如下面的代码片段所示,我们为Gux定义了三个构造函数,参数均为我们定义了服务接口类型。为了确定IServiceProvider最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IGux {} public class Foo : IFoo {} public class Bar : IBar {} public class Baz : IBaz {} public class Gux : IGux { public Gux(IFoo foo) => Console.WriteLine("Selected constructor: Gux(IFoo)"); public Gux(IFoo foo, IBar bar) => Console.WriteLine("Selected constructor: Gux(IFoo, IBar)"); public Gux(IFoo foo, IBar bar, IBaz baz) => Console.WriteLine("Selected constructor: Gux(IFoo, IBar, IBaz)"); }
在如下这段演示程序中我们创建了一个ServiceCollection对象并在其中添加针对IFoo、IBar以及IGux这三个服务接口的服务注册,针对服务接口IBaz的注册并未被添加。我们利用由它创建的IServiceProvider来提供针对服务接口IGux的实例,究竟能否得到一个Gux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢?
class Program { static void Main(string[] args) { new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient<IGux, Gux>() .BuildServiceProvider() .GetServices<IGux>(); } }
对于定义在Gux中的三个构造函数来说,由于创建IServiceProvider提供的IServiceCollection集合包含针对接口IFoo和IBar的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为IBaz的参数,这无法通过IServiceProvider来提供。根据我们上面介绍的第一个原则(IServiceProvider能够提供构造函数的所有参数),Gux的前两个构造函数会成为合法的候选构造函数,那么IServiceProvider最终会选择哪一个呢?
在所有合法的候选构造函数列表中,最终被选择出来的构造函数具有这么一个特征:每一个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。如果这样的构造函数并不存在,一个类型为InvalidOperationException的异常会被抛出来。根据这个原则,Gux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数仅仅具有一个类型为IFoo的参数,最终被选择出来的会是Gux的第二个构造函数,所有运行我们的实例程序将会在控制台上产生如图1所示的输出结果。
接下来我们对实例程序略加改动。如下面的代码片段所示,我们只为Gux定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo&IBar和IBar&IBaz。我们将针对IBaz/Baz的服务注册添加到创建的ServiceCollection对象上。
class Program { static void Main(string[] args) { new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient<IBaz, Baz>() .AddTransient<IGux, Gux>() .BuildServiceProvider() .GetServices<IGux>(); } } public class Gux : IGux { public Gux(IFoo foo, IBar bar) {} public Gux(IBar bar, IBaz baz) {} }
对于Gux的两个构造函数,虽然它们的参数均能够由IServiceProvider来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以ServiceProvider无法选择出一个最佳的构造函数。运行该程序后会抛出如图2所示的InvalidOperationException异常,并提示无法从两个候选的构造函数中选择出一个最优的来创建服务实例。
接下来我们着重介绍服务生命周期的话题。生命周期决定了IServiceProvider采用怎样的方式提供和释放服务实例。虽然不同版本的DI框架在针对服务实例生命周期管理采用了不同的实现,但总的来说,实现原理还是类似的。在我们提供的DI框架Cat中,我们已经模拟了三种生命周期模式的实现原理,接下来我们结合服务范围的概念来对这个话题做进一步讲解。
三、服务范围
对于DI框架体用的三种生命周期(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具在逻辑上具有如图3所示的“父子关系”。
图3 IServiceScope与IServiceProvider(逻辑结构)
如图3所示的树形层次结构只是一种逻辑结构,从对象引用层面来开,通过某个IServiceScope包裹的IServiceProvider对象不需要知道自己的“父亲”是谁,它只关心作为根节点的IServiceProvider在哪里就可以了。图4从物理层面揭示了IServiceScope/IServiceProvider对象之间的关系,任何一个IServiceProvider对象都具有针对根容器的引用。
图4 IServiceScope与IServiceProvider(物理结构)
四、三种生命周期模式
只有在充分了解IServiceScope的创建过程以及它与IServiceProvider之间的关系之后,我们才会对三种生命周期管理模式(Singleton、Scope和Transient)具有深刻的认识。就服务实例的提供方式来说,它们之间具有如下的差异:
-
Singleton:IServiceProvider创建的服务实例保存在作为根容器的IServiceProvider上,所有多个同根的IServiceProvider对象提供的针对同一类型的服务实例都是同一个对象。
-
Scoped:IServiceProvider创建的服务实例由自己保存,所以同一个IServiceProvider对象提供的针对同一类型的服务实例均是同一个对象。
-
Transient:针对每一次服务提供请求,IServiceProvider总是创建一个新的服务实例。
IServiceProvider除了为我们提供所需的服务实例之外,对于由它提供的服务实例,它还肩负起回收释放之责。这里所说的回收释放与.NET Core自身的垃圾回收机制无关,仅仅针对于自身类型实现了IDisposable接口的服务实例(下面简称为Disposable服务实例),针对服务实例的释放体现为调用它们的Dispose方法。IServiceProvider针对服务实例采用的回收释放策略取决于对应服务注册的生命周期模式,具体服务回收策略主要体现为如下两点:
-
Singleton:提供Disposable服务实例保存在作为根容器的IServiceProvider对象上,只有后者被释放的时候这些Disposable服务实例才能被释放。
-
Scoped和Transient:IServiceProvider对象会保存由它提供的Disposable服务实例,当自己被释放的时候,这些Disposable会被释放。
综上所述,每个作为DI容器的IServiceProvider对象都具有如图5所示两个列表来存放服务实例,我们将它们分别命名为“Realized Services”和“Disposable Services”,对于一个作为非根容器的IServiceProvider对象来说,由它提供的Scoped服务保存在自身的Realized Services列表中,Singleton服务实例则会保存在根容器的Realized Services列表。如果服务实现类型实现了IDisposable接口,Scoped和Singleton服务实例会被保存到自身的Disposable Services列表中,而Singleton服务实例则会保存到根容器的Disposable Services列表。
图5 生命周期管理
对于作为容器的IServiceProvider对象来说,Singleton和Scope模式对它来说是两种等效的生命周期模式,由它提供的Singleton和Scoped服务实例会被被存放到自身的Realized Services列表,而所有需要被释放的服务实例则被存放到Disposable Services列表。
当某个IServiceProvider被用于提供针对指定类型的服务实例时,它会根据服务类型提取出表示服务注册的ServiceDescriptor对象并根据后者得到对应的生命周期模式。如果生命周期模式为Singleton,并且作为根容器的Realized Services列表中包含对应的服务实例,后者将作为最终提供的服务实例。如果这样的服务实例尚未创建,那么新的服务将会被创建出来并作为提供的服务实例。在返回之后该对象会被添加到根容器的Realized Services列表中,如果实例类型实现了IDisposable接口,创建的服务实例会被添加到根容器的Disposable Services列表中。
如果生命周期为Scoped,那么IServiceProvider会先确定自身的Realized Services列表中是否存在对应的服务实例,存在的服务实例将作为最终返回的服务实例。如果Realized Services列表不存在对应的服务实例,那么新的服务实例会被创建出来。在作为最终的服务实例被返回之前,创建的服务实例会被添加的自身的Realized Services列表中,如果实例类型实现了IDisposable接口,创建的服务实例会被添加到自身的Disposable Services列表中。
如果提供服务的生命周期为Transient,那么IServiceProvider会直接创建一个新的服务实例。在作为最终的服务实例被返回之前,创建的服务实例会被添加的自身的Realized Services列表中,如果实例类型实现了IDisposable接口,创建的服务实例会被添加到自身的Disposable Services列表中。
对于非根容器的IServiceProvider对象来说,它的生命周期是由“包裹”着它的IServiceScope对象控制的。从上面给出的定义可以看出IServiceScope实现了IDisposable接口,Dispose方法的执行不仅标志着当前服务范围的终结,也意味着对应IServiceProvider对象生命周期的结束。
当代表服务范围的IServiceScope对象的Dispose方法被调用的时候,它会调用对应IServiceProvider的Dispose方法。一旦IServiceProvider因自身Dispose方法的调用而被释放的时候,它会从自身的Disposable Services列表中提取出所有需要被释放的服务实例,并调用它们的Dispose方法。在这之后,Disposable Services和Realized Services列表会被清空,列表中的服务实例和IServiceProvider对象自身会成为垃圾对象被GC回收。
五、ASP.NET Core应用下的生命周期
DI框架所谓的服务范围在ASP.NET Core应用中具有明确的边界,指的是针对每个HTTP请求的上下文,也就是服务范围的生命周期与每个请求上下文绑定在一起。如图6所示,ASP.NET Core应用中用于提供服务实例的IServiceProvider对象分为两种类型,一种是作为根容器并与应用具有相同生命周期的IServiceProvider,另一个类则是根据请求及时创建和释放的IServiceProvider,我们可以将它们分别称为Application ServiceProvider和Request ServiceProvider。
图6 生命周期管理
在ASP.NET Core应用初始化过程中,即请求管道构建过程中使用的服务实例都是由Application ServiceProvider提供的。在具体处理每个请求时,ASP.NET Core框架会利用注册的一个中间件来针对当前请求创建一个服务范围,该服务范围提供的Request ServiceProvider用来提供当前请求处理过程中所需的服务实例。一旦服务请求处理完成,上述的这个中间件会主动释放掉由它创建的服务范围。
六、服务范围检验
如果我们在一个ASP.NET Core应用中将一个服务的生命周期注册为Scoped,实际上是希望服务实例采用基于请求的生命周期。举个简单的例子,如果我们在一个ASP.NET Core应用中采用Entity Framework Core来访问数据库,我们一般会将对应的DbContext类型(姑且命名为FoobarDbContext)注册为一个Scoped服务,这样既可以保证在FoobarDbContext能够自同一个请求上下文中被重用,也可以确保FoobarDbContext在请求结束之后能够及时将数据库链接释放掉。
但是如果我们使用作为根容器的Application ServiceProvider来提供这个DbContext对象,意味着提供的DbContext将被保存在Application ServiceProvider的Realized Services列表中,知道应用关闭时才能被释放。即使提供该FoobarDbContext是针对请求的Request ServiceProvider,如果另一个Singleton服务(姑且命名为Foobar)具有针对它的依赖,意味着提供服务实例Foobar将会具有针对FoobarDbContext对象的引用。由于Foobar是一个Singleton服务实例,所以被它引用的FoobarDbContext也只能在应用关闭的时候才能被释放。
为了解决这个问题,我们可以让IServiceProvider在提供Scoped服务实例的时候进行针对性的检验。针对服务范围验证的开关由ServiceProviderOptions的ValidateScopes属性来控制,默认情况下是关闭的。如果希望开启针对服务范围的验证,我们可以在调用IServiceCollect接口的BuildServiceProvider方法的时候指定一个ServiceProviderOptions对象作为参数,或者直接调用另一个扩展方法并将传入的参数validateScopes设置为True。
public class ServiceProviderOptions { public bool ValidateScopes { get; set; } } public static class ServiceCollectionContainerBuilderExtensions { public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options); public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes); }
针对服务范围的验证对于IServiceProvider来说是一项额外附加的操作,会对性能带来或多或少的影响,所以一般情况下这个开关只会在开发(Development)环境被开启,对于产品(Production)或者预发(Staging)环境下最好将其关闭。
依赖注入[1]: 控制反转
依赖注入[2]: 基于IoC的设计模式
依赖注入[3]: 依赖注入模式
依赖注入[4]: 创建一个简易版的DI框架[上篇]
依赖注入[5]: 创建一个简易版的DI框架[下篇]
依赖注入[6]: .NET Core DI框架[编程体验]
依赖注入[7]: .NET Core DI框架[服务注册]
依赖注入[8]: .NET Core DI框架[服务消费]
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。