[ASP.NET Core 3框架揭秘] Options[5]: 依赖注入
《Options模型》介绍了组成Options模型的4个核心对象以及它们之间的交互关系,读者对如何得到Options对象的实现原理可能不太了解,本篇文章主要介绍依赖注入的相关内容。既然我们能够利用IServiceProvider对象提供的IOptions<TOptions>服务、IOptionsSnapshot<TOptions>服务和IOptionsMonitorCache<TOptions>服务来获取对应的Options对象,那么在这之前必然需要注册相应的服务。回顾《配置选项的正确使用方式》演示的几个实例可以发现,Options模式涉及的API其实不是很多,大都集中在相关服务的注册上。Options模型的核心服务实现在IServiceCollection接口的AddOptions扩展方法。
一、AddOptions
AddOptions扩展方法的完整定义如下所示,由此可知,该方法将Options模型中的几个核心类型作为服务注册到了指定的IServiceCollection对象之中。由于它们都是调用TryAdd方法进行服务注册的,所以我们可以在需要Options模式支持的情况下调用AddOptions方法,而不需要担心是否会添加太多重复服务注册的问题。
public static class OptionsServiceCollectionExtensions { public static IServiceCollection AddOptions(this IServiceCollection services) { services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>))); services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>))); return services; } }
从给出的代码片段可以看出,AddOptions扩展方法实际上注册了5个服务。由于这5个服务注册非常重要,所以笔者采用表格的形式列出了它们的Service Type(服务接口)、Implementation(实现类型)和Lifetime(生命周期)(见下表)。虽然服务接口IOptions<TOptions>和IOptionsSnapshot<TOptions>映射的实现类型都是OptionsManager<TOptions>,但是它们具有不同的生命周期。具体来说,前者的生命周期为Singleton,后者的生命周期则是Scoped,后续内容会单独讲述不同生命周期对Options对象产生什么样的影响。
Service Type | Implementation | Lifetime |
IOptions<TOptions> | OptionsManager<TOptions> | Singleton |
IOptionsSnapshot<TOptions> | OptionsManager<TOptions> | Scoped |
IOptionsMonitor<TOptions> | OptionsMonitor<TOptions> | Singleton |
IOptionsFactory<TOptions> | OptionsFactory<TOptions> | Transient |
IOptionsMonitorCache<TOptions> | OptionsCache<TOptions> | Singleton |
按照上表列举的服务注册,如果以IOptions<TOptions>和IOptionsSnapshot<TOptions>作为服务类型从IServieProvidere对象中提取对应的服务实例,得到的都是OptionsManager<TOptions>对象。当OptionsManager<TOptions>对象被创建时,OptionsFactory<TOptions>对象会被自动创建出来并以构造器注入的方式提供给它并且被用来创建Options对象。但是由于表7-1中并没有针对服务IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的注册,所以创建的Options对象无法被初始化。
二、Configure<TOptions>与PostConfigure<TOptions>
针对IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的服务注册是通过如下这些扩展方法来完成的。具体来说,针对IConfigureOptions<TOptions>服务的注册实现在Configure<TOptions>方法中,而PostConfigure<TOptions>扩展方法则帮助我们完成针对IPostConfigureOptions<TOptions>的注册。
public static class OptionsServiceCollectionExtensions { public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class => services.AddSingleton<IConfigureOptions<TOptions>>( new ConfigureNamedOptions<TOptions>(name, configureOptions)); return services; public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.PostConfigure(Options.Options.DefaultName, configureOptions); public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name,Action<TOptions> configureOptions) where TOptions : class => services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions)); }
从上述代码可以看出,这些方法注册的服务实现类型为ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,采用的生命周期模式均为Singleton。不论是ConfigureNamedOptions<TOptions>还是PostConfigureOptions<TOptions>,都需要指定一个具体的名称,对于没有指定具体Options名称的Configure<TOptions>和PostConfigure<TOptions>方法重载来说,最终指定的是代表默认名称的空字符串。
三、ConfigureAll<TOptions>与PostConfigureAll<TOptions>
虽然ConfigureAll<TOptions>和PostConfigureAll<TOptions>扩展方法注册的同样是ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>类型,但是它们会将名称设置为Null。通过《Options模型》的内容可知,OptionsFactory对象在进行Options对象的初始化过程中会将名称为Null的IConfigureNamedOptions<TOptions>和IPostConfigureOptions<TOptions>对象作为公共的配置对象,并且无条件执行。
public static class OptionsServiceCollectionExtensions { public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions); public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.PostConfigure(name: null, configureOptions: configureOptions); }
四、ConfigureOptions
对于上面这几个将Options类型作为泛型参数的方法来说,它们总是利用指定的Action<Options>对象来创建注册的ConfigureNamedOptions<TOptions>对象和PostConfigureOptions<TOptions>对象 。对于自定义的实现了IConfigureOptions<TOptions>接口或者IPostConfigureOptions<TOptions>接口的类型,我们可以调用如下所示的3个ConfigureOptions扩展方法来对它们进行注册。笔者在如下所示的代码片段中通过简化的代码描述了这3个扩展方法的实现逻辑。
public static class OptionsServiceCollectionExtensions { public static IServiceCollection ConfigureOptions(this IServiceCollection services, object configureInstance) { Array.ForEach(FindIConfigureOptions(configureInstance.GetType()), it => services.AddSingleton(it, configureInstance)); return services; } public static IServiceCollection ConfigureOptions(this IServiceCollection services, Type configureType) { Array.ForEach(FindIConfigureOptions(configureType), it => services.AddTransient(it, configureType)); return services; } public static IServiceCollection ConfigureOptions<TConfigureOptions>(this IServiceCollection services) where TConfigureOptions : class => services.ConfigureOptions(typeof(TConfigureOptions)); private static Type[] FindIConfigureOptions(Type type) { Func<Type, bool> valid = it => it.IsGenericType && (it.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) || it.GetGenericTypeDefinition() == typeof(IPostConfigureOptions<>)); var types = type.GetInterfaces().Where(valid).ToArray(); if (types.Any()) { throw new InvalidOperationException(); } return types; } }
五、OptionsBuilder<TOptions>
Options模式涉及针对非常多的服务注册,并且这些服务都是针对具体某个Options类型的,为了避免定义过多针对IServiceCollection接口的扩展方法,最新版本的Options模型采用Builder模式来完成相关的服务注册。具体来说,可以将用来存储服务注册的IServiceCollection集合封装到下面的OptionsBuilder<TOptions>对象中,并利用它提供的方法间接地完成所需的服务注册。
public class OptionsBuilder<TOptions> where TOptions : class { public string Name { get; } public IServiceCollection Services { get; } public OptionsBuilder(IServiceCollection services, string name); public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions); public virtual OptionsBuilder<TOptions> Configure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class; public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class; public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class; public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class; public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class; public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions); public virtual OptionsBuilder<TOptions> PostConfigure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class; public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class; public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class; public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class; public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class; public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation); public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation, string failureMessage); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation, string failureMessage); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation, string failureMessage); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation, string failureMessage); public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation, string failureMessage); }
如下面的代码片段所示,OptionsBuilder<TOptions>对象不仅通过泛型参数关联对应的Options类型,还利用Name属性提供了Options的名称。从上面的代码片段可以看出,OptionsBuilder<TOptions>类型提供的3组方法分别提供了针对IConfigureOptions<TOptions>接口、IPostConfigureOptions<TOptions>接口和IValidateOptions<TOptions>接口的18个实现类型的注册。
当利用Builder模式来注册这些服务的时候,只需要调用IServiceCollection接口的如下这两个AddOptions<TOptions>扩展方法根据指定的名称(默认名称为空字符串)创建出对应的OptionsBuilder<TOptions>对象即可。从如下所示的代码片段可以看出,这两个方法最终都需要调用非泛型的AddOptions方法,由于该方法调用TryAdd扩展方法注册Options模式的5个核心服务,所以不会导致服务的重复注册。
public static class OptionsServiceCollectionExtensions { public static OptionsBuilder<TOptions> AddOptions<TOptions>( this IServiceCollection services) where TOptions : class => services.AddOptions<TOptions>(Options.DefaultName); public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name) where TOptions : class { services.AddOptions(); return new OptionsBuilder<TOptions>(services, name); } }
六、IOptions<TOptions>与IOptionsSnapshot<TOptions>
通过对注册服务的分析可知,服务接口IOptions<TOptions>和IOptionsSnapshot<TOptions>的默认实现类型都是OptionsManager<TOptions>,两者的不同之处体现在生命周期上,前者采用的生命周期模式为Singleton,后者采用的生命周期模式则是Scoped。对于一个ASP.NET Core应用来说,Singleton和Scoped对应的是针对当前应用和当前请求的生命周期,所以通过IOptions<TOptions>接口获取的Options对象在整个应用的生命周期内都是一致的,而通过IOptionsSnapshot<TOptions>接口获取的Options对象则只能在当前请求上下文中保持一致。这也是后者命名的由来,它表示针对当前请求的Options快照 。
下面通过一个实例来演示IOptions<TOptions>和IOptionsSnapshot<TOptions>之间的差异。下面定义了FoobarOptions类型,简单起见,我们仅仅为它定义了两个整型的属性(Foo和Bar),并重写了ToString方法。
public class FoobarOptions { public int Foo { get; set; } public int Bar { get; set; } public override string ToString() => $"Foo:{Foo}, Bar:{Bar}"; }
整个演示程序体现在如下所示的代码片段中。我们创建了一个ServiceCollection对象,在调用AddOptions扩展方法注册Options模型的基础服务之后,调用Configure<FoobarOptions>方法利用定义的本地函数Print将FoobarOptions对象的Foo属性和Bar属性设置为一个随机数。
class Program { static void Main() { var random = new Random(); var serviceProvider = new ServiceCollection() .AddOptions() .Configure<FoobarOptions>(foobar => { foobar.Foo = random.Next(1, 100); foobar.Bar = random.Next(1, 100); }) .BuildServiceProvider(); Print(serviceProvider); Print(serviceProvider); static void Print(IServiceProvider provider) { var scopedProvider = provider .GetRequiredService<IServiceScopeFactory>() .CreateScope() .ServiceProvider; var options = scopedProvider .GetRequiredService<IOptions<FoobarOptions>>() .Value; var optionsSnapshot1 = scopedProvider .GetRequiredService<IOptionsSnapshot<FoobarOptions>>() .Value; var optionsSnapshot2 = scopedProvider .GetRequiredService<IOptionsSnapshot<FoobarOptions>>() .Value; Console.WriteLine($"options:{options}"); Console.WriteLine($"optionsSnapshot1:{optionsSnapshot1}"); Console.WriteLine($"optionsSnapshot2:{optionsSnapshot2}\n"); } } }
我们并没有直接利用ServiceCollection对象创建的IServiceProvider对象来提供服务,而是利用它创建了一个代表子容器的IServiceProvider对象,该对象就相当于ASP.NET Core应用中针对当前请求创建的IServiceProvider对象(RequestServices)。在利用这个IServiceProvider对象分别针对IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口得到对应的FoobarOptions对象之后,我们将配置选项输出到控制台上。上述操作先后执行了两次,相当于ASP.NET Core应用分别处理了两次请求。
下图展示了该演示程序执行后的输出结果,由此可知,只有从同一个IServiceProvider对象获取的IOptionsSnapshot<TOptions>服务才能提供一致的Options对象,但是对于所有源自同一个根的所有IServiceProvider对象来说,从中提取的IOptions<TOptions>服务都能提供一致的Options对象。
OptionsManager<Options>会利用一个自行创建的OptionsCache<TOptions>对象来缓存Options对象,也就说,OptionsManager<Options>提供的Options对象存放在其私有缓存中。虽然OptionsCache<TOptions>提供了清除缓存的能力,但是OptionsManager<Options>自身无法感知原始Options数据是否发生变化,所以不会清除缓存的Options对象。
这个特性决定了在一个ASP.NET Core应用中,以IOptions<TOptions>服务的形式提供的Options在整个应用的生命周期内不会发生改变,但是若使用IOptionsSnapshot<TOptions>服务,提供的Options对象只能在同一个请求上下文中提供一致的保障。如果希望即使在同一个请求处理周期内也能及时应用最新的Options属性,就只能使用IOptionsMonitor<TOptions>服务来提供Options对象。
[ASP.NET Core 3框架揭秘] Options[1]: 配置选项的正确使用方式[上篇]
[ASP.NET Core 3框架揭秘] Options[2]: 配置选项的正确使用方式[下篇]
[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭秘] Options[5]: 依赖注入
[ASP.NET Core 3框架揭秘] Options[6]: 扩展与定制
[ASP.NET Core 3框架揭秘] Options[7]: 与配置系统的整合