毫不夸张地说, 整个 ASP.NET Core 框架是建立在一个依赖注入框架之上的, 它在应用启动时构建请求处理管道过程中, 以及利用该管道处理每个请求过程中使用到的服务对象均来源于 DI 容器. 该 DI 容器不仅为 ASP.NET Core 框架提供必要的服务, 同时作为了应用的服务提供者, 依赖注入已经成为了 ASP.NET Core 应用基本的编程模式. 在前面一系列的文章中, 我们主要从理论层面讲述了依赖注入这种设计模式, 补充必要的理论基础是为了能够理解与 ASP.NET Core 框架无缝集成的依赖注入框架的设计原理. 我们总是采用 "先简单体验, 后者深入剖析" 来讲述每一个知识点, 所以我们利用一些简单的实例从编程层面来体验一下服务注册的添加和服务实例的提取.
一, 服务的注册与消费
为了让读者朋友们能够更加容易地认识依赖注入框架的实现原理和编程模式, 我在依赖注入[4]: 创建一个简易版的 DI 框架[上篇]和依赖注入[5]: 创建一个简易版的 DI 框架[下篇]自行创建了一个名为 Cat 的依赖注入框架. 不论是编程模式和实现原理, Cat 与我们现在即将介绍的依赖注入框架都非常相似, 对于后者提供的每一个特性, 我们几乎都能在 Cat 中找到对应物.
我在设计 Cat 的时候即将它作为提供服务实例的 DI 容器, 也作为了存放服务注册的容器, 但是与 ASP.NET Core 框架集成的这个依赖注入框架则将这两者分离开来. 我们添加的服务注册被保存到通过 IServiceCollection 接口表示的集合之中, 基于这个集合创建的 DI 容器体现为一个 IServiceProvider.
由于作为 DI 框架的 IServiceProvider 具有类似于 Cat 的层次结构, 所以两者对提供的服务实例采用一致的生命周期管理方式. DI 框架利用如下这个枚举 ServiceLifetime 提供了 Singleton,Scoped 和 Transient 三种生命周期模式是, 我在 Cat 中则将其命名为 Root,Self 和 Transient, 前者命名关注于现象, 而我则关注于内部实现.
- public enum ServiceLifetime
- {
- Singleton,
- Scoped,
- Transient
- }
应用初始化过程中添加的服务注册是 DI 容器用于提供所需服务实例的依据. 由于 IServiceProvider 总是利用指定的服务类型来提供对应服务实例, 所以服务是基于类型进行注册的, 我们倾向于利用接口来对服务进行抽象, 所以这里的服务类型一般为接口. 除了以指定服务实例的形式外(默认采用 Singleton 模式), 我们在注册服务的时候必须指定一个具体的生命周期模式.
指定注册非服务类型和实现类型;
指定一个现有的服务实例;
指定一个创建服务实例的委托对象.
我们定义了如下的接口和对应的实现类型来演示针对 DI 框架的服务注册和提取. 其中 Foo,Bar 和 Baz 分别实现了对应的接口 IFoo,IBar 和 IBaz, 为了反映 Cat 对服务实例生命周期的控制, 我们让它们派生于同一个基类 Base.Base 实现了 IDisposable 接口, 我们在其构造函数和实现的 Dispose 方法中打印出相应的文字以确定对应的实例何时被创建和释放. 我们还定义了一个泛型的接口 IFoobar<T1, T2 > 和对应的实现类 Foobar<T1, T2 > 来演示针对泛型服务实例的提供.
- public interface IFoo {}
- public interface IBar {}
- public interface IBaz {}
- public interface IFoobar<T1, T2> {}
- public class Base : IDisposable
- {
- public Base() => Console.WriteLine($"An instance of {GetType().Name} is created.");
- public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed.");
- }
- public class Foo : Base, IFoo, IDisposable { }
- public class Bar : Base, IBar, IDisposable { }
- public class Baz : Base, IBaz, IDisposable { }
- public class Foobar<T1, T2>: IFoobar<T1,T2>
- {
- public IFoo Foo { get; }
- public IBar Bar { get; }
- public Foobar(IFoo foo, IBar bar)
- {
- Foo = foo;
- Bar = bar;
- }
- }
在如下所示的代码片段中我们创建了一个 ServiceCollection(它是对 IServiceCollection 接口的默认实现)对象并调用相应的方法 (AddTransient,AddScoped 和 AddSingleton) 针对接口 IFoo,IBar 和 IBaz 注册了对应的服务, 从方法命名可以看出注册的服务采用的生命周期模式分别为 Transient,Scoped 和 Singleton. 在完成服务注册之后, 我们调用 IServiceCollection 接口的扩展方法 BuildServiceProvider 创建出代表 DI 容器的 IServiceProvider 对象, 并利用它调用后者的 GetService<T > 方法来提供相应的服务实例. 调试断言表明 IServiceProvider 提供的服务实例与预先添加的服务注册是一致的.
- class Program
- {
- static void Main()
- {
- var provider = new ServiceCollection()
- .AddTransient<IFoo, Foo>()
- .AddScoped<IBar>(_ => new Bar())
- .AddSingleton<IBaz, Baz>()
- .BuildServiceProvider();
- Debug.Assert(provider.GetService<IFoo>() is Foo);
- Debug.Assert(provider.GetService<IBar>() is Bar);
- Debug.Assert(provider.GetService<IBaz>() is Baz);
- }
- }
除了提供类似于 IFoo,IBar 和 IBaz 这样非泛型服务实例之外, 如果具有对应的泛型定义 (Generic Definition) 的服务注册, IServiceProvider 同样也能提供泛型服务实例. 如下面的代码片段所示, 在为创建的 ServiceCollection 对象添加了针对 IFoo 和 IBar 接口的服务注册之后, 我们调用 AddTransient 方法注册了针对泛型定义 IFoobar<,>的服务注册, 实现的类型为 Foobar<,>. 当我们利用 ServiceCollection 创建出代表 DI 容器的 IServiceProvider 对象并利用后者提供一个类型为 IFoobar<IFoo, IBar > 的服务实例的时候, 它会创建并返回一个 Foobar<Foo, Bar > 对象.
- var provider = new ServiceCollection()
- .AddTransient<IFoo, Foo>()
- .AddTransient<IBar, Bar>()
- .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>))
- .BuildServiceProvider();
- var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>();
- Debug.Assert(foobar.Foo is Foo);
- Debug.Assert(foobar.Bar is Bar);
当我们在进行服务注册的时候, 可以为同一个类型添加多个服务注册, 实际上添加的所有服务注册均是有效的. 不过由于扩展方法 GetService<T > 总是返回一个唯一的服务实例, 我们对该方法采用了 "后来居上" 的策略, 即总是采用最近添加的服务注册来创建服务实例. 如果我们调用另一个扩展方法 GetServices<T>, 它将利用返回所有服务注册提供的服务实例.
如下面的代码片段所示, 我们为创建的 Cat 对象添加了三个针对 Base 类型的服务注册, 对应的实现类型分别为 Foo,Bar 和 Baz. 我们最后将 Base 作为泛型参数调用了 GetServices<Base > 方法, 该方法会返回包含三个 Base 对象的集合, 集合元素的类型分别为 Foo,Bar 和 Baz.
- var services = new ServiceCollection()
- .AddTransient<Base, Foo>()
- .AddTransient<Base, Bar>()
- .AddTransient<Base, Baz>()
- .BuildServiceProvider()
- .GetServices<Base>();
- Debug.Assert(services.OfType<Foo>().Any());
- Debug.Assert(services.OfType<Bar>().Any());
- Debug.Assert(services.OfType<Baz>().Any());
对于 IServiceProvider 针对服务实例的提供还具有这么一个细节: 如果我们在调用 GetService 或者 GetService<T > 方法是将服务类型设置为 IServiceProvider 接口类型, 提供的服务实例实际上就是当前的 IServiceProvider 对象. 这一特性意味着我们可以将代表 DI 容器的 IServiceProvider 作为服务进行注入, 但是在依赖注入[3]: 依赖注入模式已经提到过, 一旦我们在应用中利用注入的 IServiceProvider 来获取其他依赖的服务实例, 意味着我们在使用 "Service Locator" 模式. 这是一种 "反模式(Anti-Pattern)", 如果迫不得已最好不要这么做. IServiceProvider 的这一特性体现在如下所示的调试断言中.
- var provider = new ServiceCollection().BuildServiceProvider();
- Debug.Assert(provider.GetService<IServiceProvider>() == provider);
二, 生命周期管理
IServiceProvider 之间的层次结构造就了三种不同的生命周期模式: 由于 Singleton 服务实例保存在作为根容器的 IServiceProvider 对象上, 所以它能够在多个同根 IServiceProvider 对象之间提供真正的单例保证. Scoped 服务实例被保存在当前 IServiceProvider 上, 所以它只能在当前 IServiceProvider 对象的 "服务范围" 保证的单例的. 没有实现 IDisposable 接口的 Transient 服务则采用 "即用即取, 用后即弃" 的策略.
接下来我们通过简单的实例来演示三种不同生命周期模式的差异. 在如下所示的代码片段中我们创建了一个 ServiceCollection 对象并针对接口 IFoo,IBar 和 IBaz 注册了对应的服务, 它们采用的生命周期模式分别为 Transient,Scoped 和 Singleton. 在利用 ServiceCollection 创建出代表 DI 容器的 IServiceProvider 对象之后, 我们调用其 CreateScope 方法创建了两个所谓的 "服务范围", 后者的 ServiceProvider 属性返回一个新的 IServiceProvider 对象, 它实际上是当前 IServiceProvider 对象的子容器. 我们最后利用作为子容器的 IServiceProvider 对象来提供相应的服务实例.
- class Program
- {
- static void Main()
- {
- var root = new ServiceCollection()
- .AddTransient<IFoo, Foo>()
- .AddScoped<IBar>(_ => new Bar())
- .AddSingleton<IBaz, Baz>()
- .BuildServiceProvider();
- var provider1 = root.CreateScope().ServiceProvider;
- var provider2 = root.CreateScope().ServiceProvider;
- void GetServices<TService>(IServiceProvider provider)
- {
- provider.GetService<TService>();
- provider.GetService<TService>();
- }
- GetServices<IFoo>(provider1);
- GetServices<IBar>(provider1);
- GetServices<IBaz>(provider1);
- Console.WriteLine();
- GetServices<IFoo>(provider2);
- GetServices<IBar>(provider2);
- GetServices<IBaz>(provider2);
- }
- }
上面的程序运行之后会在控制台上输出如图 1 所示的结果. 由于服务 IFoo 被注册为 Transient 服务, 所以 IServiceProvider 针对该接口类型的四次请求都会创建一个全新的 Foo 对象. IBar 服务的生命周期模式为 Scoped, 如果我们利用同一个 IServiceProvider 对象来提供对应的服务实例, 它只会创建一个 Bar 对象, 所以整个程序执行过程中会创建两个 Bar 对象. IBaz 服务采用 Singleton 生命周期, 所以具有同根的两个 IServiceProvider 对象提供的总是同一个 Baz 对象, 后者只会被创建一次.
图 1 IServiceProvider 按照服务注册对应的生命周期模式提供服务实例
作为 DI 容器的 IServiceProvider 不仅仅为我们提供所需的服务实例, 它还帮我们管理者这些服务实例的生命周期. 如果某个服务实例实现了 IDisposable 接口, 意味着当生命周期完结的时候需要通过调用 Dispose 方法执行一些资源释放操作, 这些操作同样由提供服务实例的 IServiceProvider 对象来驱动执行. DI 框架针对提供服务实例的释放策略取决于对应的服务注册采用的生命周期模式, 具体的策略如下:
Transient 和 Scoped: 所有实现了 IDisposable 接口的服务实例会被作为服务提供者的当前 IServiceProvider 对象保存起来, 当 IServiceProvider 对象自身被释放的时候, 这些服务实例的 Dispose 方法会随之被调用.
Singleton: 由于服务实例保存在作为根容器的 IServiceProvider 对象上, 所以后者被释放的时候调用会触发针对服务实例的释放.
对于一个 ASP.NET Core 应用来说, 它具有一个与当前应用绑定, 代表全局根容器的 IServiceProvider 对象. 对于处理的每一次请求, ASP.NET Core 框架都会利用这个根容器来创建基于当前请求的服务范围, 并利用后者提供的 IServiceProvider 来提供请求处理所需的服务实例. 请求处理完成之后, 创建的服务范围被终结, 对应的 IServiceProvider 对象也随之被释放, 此时由它提供的 Scoped 服务实例以及实现了 IDisposable 接口的 Transient 服务实例最终得以释放.
上述的释放策略可以通过如下的演示实例来印证. 我们在如下的代码片段中创建了一个 ServiceCollection 对象, 并针对不同的生命周期模式添加了针对 IFoo,IBar 和 IBaz 的服务注册. 在利用 ServiceCollection 创建出作为根容器的 IServiceProvider 之后, 我们调用它的 CreateScope 方法创建出对应的服务范围. 接下来我们利用创建对的服务范围得到代表子容器的 IServiceProvider 对象, 并用后者提供了三个注册服务对应的实例.
- class Program
- {
- static void Main()
- {
- using (var root = new ServiceCollection()
- .AddTransient<IFoo, Foo>()
- .AddScoped<IBar, Bar>()
- .AddSingleton<IBaz, Baz>()
- .BuildServiceProvider())
- {
- using (var scope = root.CreateScope())
- {
- var provider = scope.ServiceProvider;
- provider.GetService<IFoo>();
- provider.GetService<IBar>();
- provider.GetService<IBaz>();
- Console.WriteLine("Child container is disposed.");
- }
- Console.WriteLine("Root container is disposed.");
- }
- }
- }
由于代表根容器的 IServiceProvider 对象和服务范围的创建都是在 using 块中进行的, 所有针对它们的 Dispose 方法都会在 using 块结束的地方被调用, 为了确定方法被调用的时机, 我们特意在控制台上打印了相应的文字. 该程序运行之后会在控制台上输出如图 2 所示的结果, 我们可以看到当作为子容器的 IServiceProvider 对象被释放的时候, 由它提供的两个生命周期模式分别为 Transient 和 Scoped 的两个服务实例 (Foo 和 Bar) 被正常释放了. 至于生命周期模式为 Singleton 的服务实例 Baz, 它的 Dispose 方法会延迟到作为根容器 IServiceProvider 对象被释放的时候.
图 2 服务实例的释放
三, 服务范围的检验
Singleton 和 Scoped 这两种不同生命周期是通过将提供的服务实例分别存放到作为根容器的 IServiceProvider 对象和当前 IServiceProvider 对象来实现, 这意味着作为根容器的 IServiceProvider 对象提供的 Scoped 服务实例也是不能被释放的. 如果某个 Singleton 服务以来另一个 Scoped 服务, 那么 Scoped 服务实例将被一个 Singleton 服务实例所引用, 意味着 Scoped 服务实例也成了一个不会被释放的服务实例.
在 ASP.NET Core 应用中, 当我们将某个服务注册的生命周期设置为 Scoped 的真正意图是希望 DI 容器根据请求上下文来创建和释放服务实例, 但是一旦出现上述的情况下, 意味着 Scoped 服务实例将变成一个 Singleton 服务实例, 这样的 Scoped 服务实例直到应用关闭的哪一个才会得到释放. 如果某个 Scoped 服务实例引用的资源 (比如数据库连接) 需要被及时释放, 这可能会对应用造成灭顶之灾. 为了避免这种情况下, 我们在利用 IServiceProvider 提供服务过程开启针对服务范围的验证.
如果希望 IServiceProvider 在提供服务的过程中对服务范围作有效性检验, 我们只需要在调用 ServiceCollection 的 BuildServiceProvider 方法的时候将一个布尔类型的 True 值作为参数即可. 在如下所示的演示程序中, 我们定义了两个服务接口 (IFoo 和 IBar) 和对应的实现类型(Foo 和 Bar), 其中 Foo 依赖 IBar. 我们将 IFoo 和 IBar 分别注册为 Singleton 和 Scoped 服务, 当我们在调用 BuildServiceProvider 方法创建代表 DI 容器的 IServiceProvider 对象的时候将参数设置为 True 以开启针对服务范围的检验. 我们最后分别利用代表根容器和子容器的 IServiceProvider 来分别提供这两种类型的服务实例.
- class Program
- {
- static void Main()
- {
- var root = new ServiceCollection()
- .AddSingleton<IFoo, Foo>()
- .AddScoped<IBar, Bar>()
- .BuildServiceProvider(true);
- var child = root.CreateScope().ServiceProvider;
- void ResolveService<T>(IServiceProvider provider)
- {
- var isRootContainer = root == provider ? "Yes" : "No";
- try
- {
- provider.GetService<T>();
- Console.WriteLine( $"Status: Success; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Status: Fail; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
- Console.WriteLine($"Error: {ex.Message}");
- }
- }
- ResolveService<IFoo>(root);
- ResolveService<IBar>(root);
- ResolveService<IFoo>(child);
- ResolveService<IBar>(child);
- }
- }
- public interface IFoo {}
- public interface IBar {}
- public class Foo : IFoo
- {
- public IBar Bar { get; }
- public Foo(IBar bar) => Bar = bar;
- }
- public class Bar : IBar {}
上面这个演示实例启动之后将在控制台上输出如图 3 所示的输出结果. 从输出结果可以看出针对四个服务解析, 只有一次 (使用代表子容器的 IServiceProvider 提供 IBar 服务实例) 是成功的. 这个实例充分说明了一旦开启了针对服务范围的验证, IServiceProvider 对象不可能提供以单例形式存在的 Scoped 服务.
图 3 IServiceProvider 针对服务范围的检验
来源: https://www.cnblogs.com/artech/p/net-core-di-06.html