通过这篇的文章,我们了解到了一个依赖注入容器需要用到的几个概念和一些反射的知识。接下来,我们解析一下.net下依赖注入的设计。
.net下的依赖注入相关的NuGet包有两个,一个是Microsoft.Extensions.DependencyInjection.Abstractions,另一个是Microsoft.Extensions.DependencyInjection。前者是抽象,包含了接口定义和一些公共的基础数据类型。后者是抽象的实现,里面有DI容器的默认实现等等。
核心对象
在设计Cat时,我们将它设计成服务提供容器和服务注册的结合体。但是.NET下面没有这么做,服务注册是由IServiceCollections这个接口来负责的,而服务提供容器则由IServiceProvidere这个接口来提供。
作为依赖注入容器的IServiceProvider在结构上与Cat保持高度的一致,Cat中定义的Lifetime枚举与.NET下定义的ServiceLifetime对应关系为Singleton<–>Root;Scoped<–>Self;Transient<–>Transient.
在http://asp.net core中DI的概念是由这几部分组成的:
IServiceCollection,保存IServiceDescriptor实例的列表
IServiceProvider,只有一个方法GetService,用来获取服务。ServiceProvider有一个Scope的概念,是通过CreateScope(它又调用了IServiceScopeFactory)来创建一个IServiceScope,这个IServiceScope中包含一个新创建的IServiceProvider。
IServiceDescriptor,用来描述服务,包括Service Type,Implementation Type,Service LifeTime,以及创建服务实现的Factory。
ServiceLifeTime,描述服务的生命周期,有三种生命周期:
Singleton
Scope
Transient
DI就是由上述这四个主要的类型外加一大堆扩展方法而形成的。
作为服务提供容器的IServiceProvider,它不仅要提供服务,还要管理服务的生命周期。ServiceProvider通过一种层次结构来配合三种生命周期达到这个目的:作为Singleton的服务通过根容器销毁,而作为Scoped和Transient的服务通过当前容器销毁。
生命周期
Singleton
单例模式。IServiceProvider作为DI容器,单例模式的服务需要由根容器销毁。它的生命周期会随根容器的销毁而销毁,也就是说,应用程序停掉了它才会销毁。
Scope
Scope实际上是由一个IServiceScope接口来描述的,上面有写,它是由ServiceProvider的一个扩展方法CreateScope来实现的,内部由IserviceScopeFacrory来最终生成一个Scope,这个Scope生成的时候会创建一个新的ServiceProvider,利用新生成的ServiceProvider来提供的服务也就形成了一个Scope的概念。顺便插一句,任何一个ServiceProvider都有一个根容器的引用,所有的Provider都引用了同一个根容器。它的生命周期随创建它的Scope的销毁而销毁,在http://asp.net core里面,当客户端发起请求时,都会新创建一个Scope,然后都是利用这个新创建的Scope中的ServiceProvider属性来提供服务的。
Transient
随用随取,用完就销毁。它的生命周期通Scope,都是随创建它的ServiceProvider的销毁而销毁。
Scope的特殊说明
在ASP.NETCore应用中,将某个服务注册的生命周期设置为Scoped的真正意图是希望依赖注入容器根据接收的每个请求来创建和释放服务实例,但是一旦出现上述这种情况,就意味着Scoped服务实例将变成一个Singleton服务实例,这样的Scoped服务实例直到应用关闭才会被释放,这无疑不是我们希望得到的结果。如果某个Scoped服务实例引用的资源(如数据库连接)需要被及时释放,这可能会造成难以估量的后果。为了避免这种情况的出现,在利用IServiceProvider对象提供服务的过程中可以开启针对服务范围的验证。
一般情况下,生命周期较长的服务不应该依赖一个生命周期较短的服务,BuildServiceProvider方法可以传入一个bool类型的参数,这个参数解决了这么一个问题:
举个例子,比方说在http://asp.net core项目中我们使用entity framework core,要添加一个DbContext,这个DbContext是被添加成Scope生命周期的,也就是说它会随着http请求的发起创建,请求结束后消亡。但是如果他被一个Singleton的服务所依赖/引用,那么它的生命周期就会变成Singleton。这个连接永远都不会释放,当许多请求过来的时候,数据库的连接池的连接就会变得不够用。这本身是一种错误的实践,避免这样的错误就是在BuilServiceProvider这个方法中传入一个true的bool值,指示DI在注入服务的时候要判断Scope的服务是否被错误的使用,一般来说错误的使用有两种情况:
第一种情况是用根ServiceProvider来创建一个Scope的服务。在根容器上面创建的服务会由根容器来负责销毁,那么也就是说,Scope的服务也就变成了Singleton的了。因为根容器只有在应用程序停止运行的时候才会销毁。
第二种情况是Scope的服务被Singleton的服务依赖/引用。那么这个Scope的服务也会随依赖它的Singleton服务变成Singleton的。
扩展点
.NETCore具有一个承载(Hosting)系统,该系统承载需要在后台长时间运行的服务,一个http://ASP.NET Core应用仅仅是该系统承载的一种服务(IHostedService)而已。承载系统总是采用依赖注入的方式消费它在服务承载过程中所需的服务。对于承载系统来说,原始的服务注册总是体现为一个IServiceCollection集合,最终的依赖注入容器则体现为一个IServiceProvider对象,如果要将第三方依赖注入框架整合进来,就需要利用它们解决从IServiceCollection集合到IServiceProvider对象的适配问题。
具体来说,我们可以在IServiceCollection集合和IServiceProvider对象之间设置一个针对某个第三方依赖注入框架的ContainerBuilder对象:先利用包含原始服务注册的IServiceCollection集合创建一个ContainerBuilder对象,再利用该对象构建作为依赖注入容器的IServiceProvider对象。
而解决从IServiceCollection到IServiceProvider的这个过程也有一个标准的接口,叫做IServiceProviderFactory。
public interface IServiceProviderFactory<TContainerBuilder> where TContainerBuilder : notnull
{
//
// 摘要:
// Creates a container builder from an Microsoft.Extensions.DependencyInjection.IServiceCollection.
//
// 参数:
// services:
// The collection of services
//
// 返回结果:
// A container builder that can be used to create an System.IServiceProvider.
TContainerBuilder CreateBuilder(IServiceCollection services);
//
// 摘要:
// Creates an System.IServiceProvider from the container builder.
//
// 参数:
// containerBuilder:
// The container builder
//
// 返回结果:
// An System.IServiceProvider
IServiceProvider CreateServiceProvider(TContainerBuilder containerBuilder);
}
如上所示,IServiceProviderFactory的第一个方法将传入的IServiceCollection转换为一个TContainerBuilder,在将这个TContainerBuilder在第二个方法中转换为IServiceProvider,这样,就完成了从IServiceCollection到IServiceProvider的转换。
有兴趣的同学可以查看一下DefaultServiceProviderFactory的代码,它是http://asp.net core中默认使用的。