整个 ASP.NET Core 是建立在依赖注入框架之上的。
3.1 利用容器提供服务
本章主要介绍这个独立的基础框架,不涉及它在 ASP.NET Core 框架中的应用。
3.1.1 服务的注册与消费
这个框架主要涉及两个 NuGet 包,接口和类型都定义在 “Microsoft.Extensions.DependencyInjection.Abstraction” 包,具体实现在“Microsoft.Extensions.DependencyInjection”包。
添加的服务注册被保存在 IServiceCollection 接口表示的集合中,并通过集合创建表示依赖注入容器的 IServiceProvider 对象。
依赖注入框架使用 ServiceLifetime 枚举表示 Singleton,Scoped 和 Transient 这三种生命周期模式。
具体的服务注册方式主要体现为以下三种形式:
1)指定具体的服务实现类型;
2)提供一个现成的服务实例;
3)指定一个创建服务实例的工厂。
1 var provider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar>(_ => new Bar()) 4 .AddSingleton<IBaz, Baz>() 5 .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) 6 .BuildServiceProvider(); 7 8 Debug.Assert(provider.GetService<IFoo>() is Foo); 9 Debug.Assert(provider.GetService<IBar>() is Bar); 10 Debug.Assert(provider.GetService<IBaz>() is Baz); 11 12 var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>(); 13 14 Debug.Assert(foobar?.Foo is Foo); 15 16 provider.Dispose();
输出如下:
可以为同一类型添加多个服务注册,所有服务注册都是有效的,但 GetService<T> 扩展方法只能返回一个实例,框架采用的是“后来居上”的策略。GetServices<T> 扩展方法将指定服务类型的所有注册服务来提供一组服务实例。
1 var baseServices = provider.GetServices<Base>(); 2 Debug.Assert(baseServices.OfType<Foo>().Any()); 3 Debug.Assert(baseServices.OfType<Bar>().Any()); 4 Debug.Assert(baseServices.OfType<Baz>().Any());
甚至,调用 GetService<T> 或 GetServices<T> 时将 T 设置为 IServiceProvider,我们可以得到容器对象本身。
3.1.2 生命周期
表示依赖注入容器的 IServiceProvider 对象之间的层次关系组成了服务实例的 3 种生命周期。Singleton 服务实例保存在作为根容器的 IServiceProvider 对象上,所以能在多个同根 IServiceProvider 对象之间保证真正的单例。Scoped 服务实例被保存在当前服务范围对应的 IServiceProvider 上,从而保证当前服务范围之内提供单例。对应类型没有实现 IDisposable 的 Transient 服务实例,遵循“即用即建,用后即弃”的渣男策略。
1 var provider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar>(_ => new Bar()) 4 .AddSingleton<IBaz, Baz>() 5 .BuildServiceProvider(); 6 7 Console.WriteLine("Root provider start working.."); 8 9 provider.GetService<IFoo>(); 10 provider.GetService<IBar>(); 11 provider.GetService<IBaz>(); 12 13 Console.WriteLine(); 14 15 Console.WriteLine("Child1 provider start working.."); 16 17 var childProvider1 = provider.CreateScope().ServiceProvider; 18 var childProvider2 = provider.CreateScope().ServiceProvider; 19 20 GetServices<IFoo>(childProvider1); 21 GetServices<IBar>(childProvider1); 22 GetServices<IBaz>(childProvider1); 23 24 Console.WriteLine(); 25 26 Console.WriteLine("Child2 provider start working.."); 27 28 GetServices<IFoo>(childProvider2); 29 GetServices<IBar>(childProvider2); 30 GetServices<IBaz>(childProvider2);
容器不仅负责构建并提供服务实例,还负责管理服务实例的生命周期。策略如下:
1)Transient 和 Scoped:所有实现了 IDisposable 接口的服务实例会被当前 IServiceProvider 对象保存,当 IServiceProvider 的 Dispose 被调用时,这些服务实例 的 Dispose 会随之被调用。
2)Singleton:当根容器的 Dispose 被调用时,这些服务实例的 Dispose 才会随之被调用。
根容器与应用具有一致的生命周期因而被称为 ApplicationServices。服务范围所在的 IServiceProvider 对象被称为 RequestServices,请求处理完成之后(怎么算完成?比如使用 using),RequestServices 被释放,在该范围内创建的 Scoped 服务实例和实现了 IDisposable 接口的 Transient 服务实例被及时释放。
1 using(var rootProvider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar, Bar>() 4 .AddSingleton<IBaz, Baz>() 5 .BuildServiceProvider()){ 6 7 using(var scope = rootProvider.CreateScope()){ 8 var scopedProvider = scope.ServiceProvider; 9 10 scopedProvider.GetService<IFoo>(); 11 scopedProvider.GetService<IBar>(); 12 scopedProvider.GetService<IBaz>(); 13 14 Console.WriteLine("Scoped container is disposing..."); 15 } 16 17 Console.WriteLine("Root container is disposing..."); 18 }
3.1.3 服务注册的验证
根容器提供的 Scoped 服务也是单例的。如果一个单例服务依赖另一个 Scoped 服务,那么这个 Scoped 服务会被一个 Singleton 服务所引用,这意味着这个 Scoped 服务实例也成为一个单例服务。
1 using(var rootProvider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddScoped<IBar, Bar>() 4 .AddSingleton<IBaz, Baz>() 5 .AddSingleton(typeof(IFoobar<,>), typeof(Foobar<,>)) 6 .BuildServiceProvider()){ 7 using(var scope = rootProvider.CreateScope()){ 8 var scopedProvider = scope.ServiceProvider; 9 10 Console.WriteLine("Scoped container created instances.."); 11 12 scopedProvider.GetService<IFoo>(); 13 scopedProvider.GetService<IBar>(); 14 scopedProvider.GetService<IBaz>(); 15 16 var foobar = (Foobar<IFoo, IBar>)scopedProvider.GetService<IFoobar<IFoo, IBar>>(); 17 18 Console.WriteLine("Scoped container is disposing..."); 19 } 20 21 Console.WriteLine(); 22 23 Console.WriteLine("Root container is disposing..."); 24 }
注意第 16 行代码,这里的单例 IFoobar<,> 引用了 transient 服务 IFoo 和 scoped 服务 IBar,看输出可以发现在根容器 Dispose 时的信息输出。
在 ASP.NET Core 应用中,一般只会将请求具有一致生命周期的服务注册为 Scope 模式。一旦出现上面提到的情况,可能会造成严重的内存泄漏问题,框架提供了对应的验证机制。只需要在调用 BuildServiceProvider 方法时提供一个参数为 true 即可。
这里的运行结果和书上的不一致,只有前两次次是失败了,目前还不知道原因。
服务范围的检验体现在 ServiceProviderOptions 配置选项的 ValidateScopes 属性上。ServiceProviderOptions 还有一个属性名为 ValidateOnBuild 的属性。如果设置为 true,意味着 IServiceProvider 对象被构建时会对每个 ServiceDescriptor 对象实施有效性验证。
1 try{ 2 var options = new ServiceProviderOptions{ 3 ValidateOnBuild = true 4 }; 5 6 var provider = new ServiceCollection() 7 .AddSingleton<IFoo, Foo2>() 8 .BuildServiceProvider(options); 9 10 Console.WriteLine($"Status: Success."); 11 } 12 catch(Exception ex){ 13 Console.WriteLine($"Status: Fail."); 14 Console.WriteLine($"Error: {ex.Message}"); 15 }
这里用到的 Foo2 只有一个私有构造器,因此创建容器时报错。
3.2 服务注册
IServiceCollection 对象是一个存放服务注册信息的集合,具体的服务注册体现为 ServiceDescriptor 对象。
3.2.1 ServiceDescriptor
ServiceDescriptor 是对单个服务注册项的描述。
采用现成的服务实例创建的 ServiceDescriptor 对象默认采用 Singleton 生命周期模式。对于其它两种形式创建 ServiceDescriptor 对象,需要显式指定生命周期模式。
可以利用定义在 ServiceDescriptor 类型中的一系列静态方法来创建 ServiceDescriptor 对象,比如 Describe 方法。
3.2.2 IServiceCollection
服务注册的本质是将创建的 ServiceDescriptor 对象添加到指定 IServiceCollection 集合中的过程。
为了避免重复注册,TryAdd 扩展方法只会在指定类型的服务注册不存在的前提下才将 ServiceDescriptor 对象添加到集合中。TryAddEnumerable 扩展方法在用于重复性检验时会同时考虑服务类型和实现类型。
想测试扩展方法但没有找到。。,是扩展方法在不同的包吗?发现命名空间不同,需要添加“using Microsoft.Extensions.DependencyInjection.Extensions;”。
1 var services = new ServiceCollection(); 2 services.TryAdd(ServiceDescriptor.Describe(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton)); 3 Console.WriteLine(services.Count); 4 services.TryAdd(ServiceDescriptor.Describe(typeof(IFoo), typeof(Foo2), ServiceLifetime.Scoped)); 5 Console.WriteLine(services.Count); 6 7 services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo, Foo2>()); 8 Console.WriteLine(services.Count); 9 10 services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo, Foo2>()); 11 Console.WriteLine(services.Count);
IServiceCollection 实现了 IList<ServiceDescriptor>接口,所以可以调用 Clear、Remove、RemoveAll 方法删除现有的 ServiceDescriptor 对象。还有扩展方法可以删除指定的服务类型的 ServiceDescriptor 对象。
3.3 服务的消费
3.3.1 IServiceProvider
如果对应的服务注册不存在,GetService 方法会返回 Null。但调用 GetRequiredService 或 GetRequiredService<T> 会抛出一个 InvalidOperationException。
作者不打算详细介绍 ServiceProvider 类型,因为在该类型中提供服务实例的机制一直在变化。
3.3.2 服务实例的创建
ServiceDescriptor 对象具有 3 种构建方式,分别对应服务实例的 3 种提供方式。
如果提供的是服务的实现类型,那么最终提供的服务实例将通过该类型的某个构造函数来创建,那么是根据什么策略选的构造函数? 传入构造函数的所有参数必须先被初始化。在所有合法的候选构造函数列表中,最终被选中的构造函数具有如下特征:所有候选构造函数的参数类型都能在这个容器中(原文为构造函数中,感觉不对)找到,如果这样的构造函数不存在,那么抛出 InvalidOperationException。
如果有两个符合条件的参数个数相等的构造函数,也会抛出 InvalidOperationException,具体信息为“Unable to activate type 'MM.Martin.Qux'. The following constructors are ambiguous”。
3.3.3 生命周期
生命周期决定了 IServiceProvider 怎么提供和释放服务实例。
1. 服务范围
Scoped 是指由 IServiceScope 接口表示的服务范围,该范围由 IServiceScopeFactory 对象构建。
如果释放过程中涉及一些异步操作,则相应的类型往往会实现 IAsyncDisposable 接口,所以服务范围也有一个通过 AsyncServiceScope 表示的异步版本。
AsyncServiceScope 是一个只读的 struct,派生于 IServiceScope 接口,同时实现了 IAsyncDisposable 接口。本质上是对一个 IServiceScope 的封装。
IServiceScopeFactory 和 IServiceProvider 都定义了 CreateAsyncScope 扩展方法创建表示异步服务范围的 AsyncServiceScope 。
2. 3 种生命周期模式
IServiceProvider 负责在服务实例在其生命周期终结时释放服务实例(如果需要),这里所说的释放和垃圾回收无关。
每个作为依赖注入容器的 IServiceProvider 对象都具有两个列表来存储服务实例,Realized Services 和 Disposable Services。
当 IServiceScope 对象的 Dispose 被调用时,当前范围的 IServiceProvider 的 Dispose 也会被调用,后者从自身的 Disposable Services 列表中提取所有服务实例并调用它们的 Dispose 。在这之后,两个列表同时被清空,列表中的服务实例和 IServiceProvider 自身会成为垃圾对象被 GC 回收。
当 IServiceScope 对象的 Dispose 被调用时,如果服务实例仅仅实现了 IAsyncDisposable,那么会抛出 InvalidOperationException。当以异步方式释放容器时,可以采用同步的方式释放服务实例,反之不行。
3.3.4 ActivatorUtilities
有时需要利用容器创建一个对应类型不曾注册的实例,典型的实例就是 MVC 的 Controller。这时就会利用 ActivatorUtilities 这个静态的工具类型,要求容器能否提供构造函数中必要的参数。
1 var provider = new ServiceCollection() 2 .AddTransient<IFoo, Foo>() 3 .AddTransient<IBar, Bar>() 4 .BuildServiceProvider(); 5 6 var test = ActivatorUtilities.CreateInstance<Test>(provider, "Martin Fu"); 7 Console.WriteLine(test);
这里也会选择一个合适的构造函数。如果目标类型定义了多个公共构造函数,那么取决于两个因素:显式指定的参数列表和构造函数的定义顺序。首先会遍历每一个候选的公共构造函数,并对它们创建 ConstructorMatcher 对象,然后将显式指定的参数列表作为参数调用其 Match 方法,该方法返回的数字表示匹配度,值越大匹配度越高,-1 表示完全不匹配。
由于构造函数的定义顺序有影响,所以当希望 ActivatorUtilities 选择某个构造函数时,可以设置 ActivatorUtlitiesConstructorAttribute 特性。
1 public class Test{ 2 private string Name{get;set;} 3 private IFoo Foo{get;set;} 4 private IBar Bar{get;set;} 5 6 public Test(IFoo foo, IBar bar){ 7 Console.WriteLine("2-1"); 8 this.Foo = foo; 9 this.Bar = bar; 10 } 11 12 public Test(string name, IFoo foo){ 13 Console.WriteLine("2-2"); 14 this.Name = name; 15 this.Foo = foo; 16 } 17 18 [ActivatorUtilitiesConstructor] 19 public Test(string name, IFoo foo, IBar bar){ 20 Console.WriteLine("3"); 21 this.Name = name; 22 this.Foo = foo; 23 this.Bar = bar; 24 } 25 }
3.4 扩展
作者认为原生的依赖注入框架就是最好的选择,但也可以通过扩展来整合别的开源框架。
3.4.1 适配
对承载系统来说,原始的服务注册和最终的依赖注入容器分别体现为一个 IServiceCollection 对象和 IServiceProvider 对象,而且承载系统在初始化过程中会将自身的服务注册添加到由它创建的 IServiceCollection 集合中。因此要将第三方依赖注入框架整合进来,需要解决 IServiceCollection 与 IServiceProvider 的适配问题。
3.4.2 IServiceProviderFactory<TContainerBuilder>
CreateBuilder 方法利用指定的 IServiceCollection 集合创建对应的 TContainerBuilder 对象,CreateServiceProvider 方法进一步利用 TContainerBuilder 创建作为容器的 IServiceProvider 。默认注册的是 DefaultServiceProviderFactory。