包含服务注册信息的 IServiceCollection 对象最终被用来创建作为 DI 容器的 IServiceProvider 对象. 当需要消费某个服务实例的时候, 我们只需要指定服务类型调用 IServiceProvider 的 GetService 方法, IServiceProvider 就会根据对应的服务注册提供所需的服务实例.
目录
一, 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 所示的输出结果.
图 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 异常, 并提示无法从两个候选的构造函数中选择出一个最优的来创建服务实例.
图 2 构造函数的选择策略
接下来我们着重介绍服务生命周期的话题. 生命周期决定了 IServiceProvider 采用怎样的方式提供和释放服务实例. 虽然不同版本的 DI 框架在针对服务实例生命周期管理采用了不同的实现, 但总的来说, 实现原理还是类似的. 在我们提供的 DI 框架 Cat http://mp.weixin.qq.com/s?__biz=MzIwOTM1MjgzMA==&mid=2247484139&idx=1&sn=800aefd616775399f31788666d681478&chksm=977463faa003eaec679907cd53f924d9b13009b9349b91ed9aaa01652229ce188b26cf386901&scene=21#wechat_redirect 中, 我们已经模拟了三种生命周期模式的实现原理, 接下来我们结合服务范围的概念来对这个话题做进一步讲解.
三, 服务范围
对于 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) 环境下最好将其关闭.
来源: https://www.cnblogs.com/artech/p/net-core-di-08.html