[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
通过前面演示的几个实例(配置选项的正确使用方式[上篇]、配置选项的正确使用方式[下篇]),我们已经对基于Options的编程方式有了一定程度的了解,下面从设计的角度介绍Options模型。我们演示的实例已经涉及Options模型的3个重要的接口,它们分别是IOptions<TOptions>和IOptionsSnapshot<TOptions>,最终的Options对象正是利用它们来提供的。在Options模型中,这两个接口具有同一个实现类型OptionsManager<TOptions>。Options模型的核心接口和类型定义在NuGet包“Microsoft.Extensions.Options”中。
一、OptionsManager<TOptions>
在Options模式的编程中,我们会利用作为依赖注入容器的IServiceProvider对象来提供IOptions<TOptions>服务或者IOptionsSnapshot<TOptions>服务,实际上,最终得到的服务实例都是一个OptionsManager<TOptions>对象。在Options模型中,OptionsManager<TOptions>相关的接口和类型主要体现在下图中。
下面以上图为基础介绍OptionsManager<TOptions>对象是如何提供Options对象的。如下面的代码片段所示,IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口的泛型参数的TOptions类型要求具有一个默认的构造函数,也就是说,Options对象可以在无须指定参数的情况下直接采用new关键字进行实例化,实际上,Options最初就是采用这种方式创建的。
public interface IOptions<out TOptions> where TOptions: class, new() { TOptions Value { get; } } public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions> where TOptions: class, new() { TOptions Get(string name); }
IOptions<TOptions>接口通过Value属性提供对应的Options对象,继承它的IOptionsSnapshot<TOptions>接口则利用其Get方法根据指定的名称提供对应的Options对象。OptionsManager<TOptions>针对这两个接口成员的实现依赖其他两个对象,分别通过IOptionsFactory<TOptions>接口和IOptionsMonitorCache<TOptions>接口表示,这也是Options模型的两个核心成员。
作为Options对象的工厂,IOptionsFactory<TOptions>对象负责创建Options对象并对其进行初始化。出于性能方面的考虑,由IOptionsFactory<TOptions>工厂创建的Options对象会被缓存起来,针对Options对象的缓存就由IOptionsMonitorCache<TOptions>对象负责。下面会对IOptionsFactory<TOptions>和IOptionsMonitorCache<TOptions>进行单独讲解,在此之前需要先了解OptionsManager<TOptions>类型是如何定义的。
public class OptionsManager<TOptions> :IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new() { private readonly IOptionsFactory<TOptions> _factory; private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); public OptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory; public TOptions Value => this.Get(Options.DefaultName); public TOptions Get(string name) => _cache.GetOrAdd(name, () => _factory.Create(name)); } public static class Options { public static readonly string DefaultName = string.Empty; }
OptionsManager<TOptions>对象提供Options对象的逻辑基本上体现在上面给出的代码中。在创建一个OptionsManager<TOptions>对象时需要提供一个IOptionsFactory<TOptions>工厂,而它自己还会创建一个OptionsCache<TOptions>(该类型实现了IOptionsMonitorCache<TOptions>接口)对象来缓存Options对象,也就是说,Options对象实际上是被OptionsManager<TOptions>对象以“独占”的方式缓存起来的,后续内容还会提到这个设计细节。
从编程的角度来讲,IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口分别体现了非具名与具名的Options提供方式,但是对于同时实现这两个接口的OptionsManager<TOptions>来说,提供的Options都是具名的,唯一的不同之处在于以IOptions<TOptions>接口名义提供Options对象时会采用一个空字符串作为名称。默认Options名称可以通过静态类型Options的只读字段DefaultName来获取。
OptionsManager<TOptions>针对Options对象的提供(具名或者非具名)最终体现在其实现的Get方法上。由于Options对象缓存在自己创建的OptionsCache<TOptions>对象上,所以它只需要将指定的Options名称作为参数调用其GetOrAdd方法就能获取对应的Options对象。如果Options对象尚未被缓存,它会利用作为参数传入的Func<TOptions>委托对象来创建新的Options对象,从前面给出的代码可以看出,这个委托对象最终会利用IOptionsFactory<TOptions>工厂来创建Options对象。
二、IOptionsFactory<TOptions>
顾名思义,IOptionsFactory<TOptions>接口表示创建和初始化Options对象的工厂。如下面的代码片段所示,该接口定义了唯一的Create方法,可以根据指定的名称创建对应的Options对象。
public interface IOptionsFactory<TOptions> where TOptions: class, new() { TOptions Create(string name); }
OptionsFactory<TOptions>OptionsFactory<TOptions>是IOptionsFactory<TOptions>接口的默认实现。OptionsFactory<TOptions>对象针对Options对象的创建主要分3个步骤来完成,笔者将这3个步骤称为Options对象相关的“实例化”、“初始化”和“验证”。由于Options类型总是具有一个公共默认的构造函数,所以OptionsFactory<TOptions>的实现只需要利用new关键字调用这个构造函数就可以创建一个空的Options对象。当Options对象被实例化之后,OptionsFactory<TOptions>对象会根据注册的一些服务对其进行初始化。Options模型中针对Options对象初始化的工作由如下3个接口表示的服务负责。
public interface IConfigureOptions<in TOptions> where TOptions: class { void Configure(TOptions options); } public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class { void Configure(string name, TOptions options); } public interface IPostConfigureOptions<in TOptions> where TOptions : class { void PostConfigure(string name, TOptions options); }
上述3个接口分别通过定义的Configure方法和PostConfigure方法对指定的Options对象进行初始化,其中,IConfigureNamedOptions<TOptions>和IPostConfigureOptions<TOptions>还指定了Options的名称。由于IConfigureOptions<TOptions>接口的Configure方法没有指定Options的名称,意味着该方法仅仅用来初始化默认的Options对象,而这个默认的Options对象就是以空字符串命名的Options对象。从接口命名就可以看出定义其中的3个方法的执行顺序:定义在IPostConfigureOptions<TOptions>中的PostConfigure方法会在IConfigureOptions<TOptions>和IConfigureNamedOptions<TOptions>的Configure方法之后执行。
当注册的IConfigureNamedOptions<TOptions>服务和IPostConfigureOptions<TOptions>服务完成了对Options对象的初始化之后,IOptionsFactory<TOptions>对象还应该验证最终得到的Options对象是否有效。针对Options对象有效性的验证由IValidateOptions<TOptions>接口表示的服务对象来完成。如下面的代码片段所示,IValidateOptions<TOptions>接口定义的唯一的方法Validate用来对指定的Options对象(参数options)进行验证,而参数name则代表Options的名称。
public interface IValidateOptions<TOptions> where TOptions : class { ValidateOptionsResult Validate(string name, TOptions options); } public class ValidateOptionsResult { public static readonly ValidateOptionsResult Success; public static readonly ValidateOptionsResult Skip; public static ValidateOptionsResult Fail(string failureMessage); public bool Succeeded { get; protected set; } public bool Skipped { get; protected set; } public bool Failed { get; protected set; } public string FailureMessage { get; protected set; } }
Options的验证结果由ValidateOptionsResult类型表示。总的来说,针对Options对象的验证会产生3种结果,即成功、失败和忽略,它们分别通过3个对应的属性来表示(Succeeded、Failed和Skipped)。一个表示验证失败的ValidateOptionsResult对象会通过其FailureMessage属性来描述具体的验证错误。可以调用两个静态只读字段Success和Skip以及静态方法Fail得到或者创建对应的ValidateOptionsResult对象。
Options模型提供了一个名为OptionsFactory<TOptions>的类型作为IOptionsFactory<TOptions>接口的默认实现。对上述3个接口有了基本了解后,对实现在OptionsFactory<TOptions>类型中用来创建并初始化Options对象的实现逻辑比较容易理解了。下面的代码片段基本体现了OptionsFactory<TOptions>类型的完整定义。
public class OptionsFactory<TOptions> :IOptionsFactory<TOptions> where TOptions : class, new() { private readonly IEnumerable<IConfigureOptions<TOptions>> _setups; private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures; private readonly IEnumerable<IValidateOptions<TOptions>> _validations; public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, null) { } public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations) { _setups = setups; _postConfigures = postConfigures; _validations = validations; } public TOptions Create(string name) { //步骤1:实例化 var options = new TOptions(); //步骤2-1:针对IConfigureNamedOptions<TOptions>的初始化 foreach (var setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } //步骤2-2:针对IPostConfigureOptions<TOptions>的初始化 foreach (var post in _postConfigures) { post.PostConfigure(name, options); } //步骤3:有效性验证 var failedMessages = new List<string>(); foreach (var validator in _validations) { var reusult = validator.Validate(name, options); if (reusult.Failed) { failedMessages.Add(reusult.FailureMessage); } } if (failedMessages.Count > 0) { throw new OptionsValidationException(name, typeof(TOptions), failedMessages); } return options; } }
如上面的代码片段所示,调用构造函数创建OptionsFactory<TOptions>对象时需要提供IConfigureOptions<TOptions>对象、IPostConfigureOptions<TOptions>对象和IValidateOptions<TOptions>对象。在实现的Create方法中,它首先调用默认构造函数创建一个空Options对象,再先后利用IConfigureOptions<TOptions>对象和IPostConfigureOptions<TOptions>对象对这个Options对象进行“再加工”。这一切完成之后,指定的IValidateOptions<TOptions>会被逐个提取出来对最终生成的Options对象进行验证,如果没有通过验证,就会抛出一个OptionsValidationException类型的异常。图7-8所示的UML展示了OptionsFactory<TOptions>针对Options对象的初始化。
三、ConfigureNamedOptions<TOptions>
对于上述3个用来初始化Options对象的接口,Options模型均提供了默认实现,其中,ConfigureNamedOptions<TOptions>类同时实现了IConfigureOptions<TOptions>和IConfigureNamedOptions<TOptions>接口。当我们创建这样一个对象时,需要指定Options的名称和一个用来初始化Options对象的Action<TOptions>委托对象。如果指定了一个非空的名称,那么提供的委托对象将会用于初始化与该名称相匹配的Options对象;如果指定的名称为Null(不是空字符串),就意味着提供的初始化操作适用于所有同类的Options对象。
public class ConfigureNamedOptions<TOptions> :IConfigureNamedOptions<TOptions>,IConfigureOptions<TOptions> where TOptions : class { public string Name { get; } public Action<TOptions> Action { get; } public ConfigureNamedOptions(string name, Action<TOptions> action) { Name = name; Action = action; } public void Configure(string name, TOptions options) { if (Name == null || name == Name) { Action?.Invoke(options); } } public void Configure(TOptions options) => Configure(Options.DefaultName, options); }
有时针对某个Options的初始化工作需要依赖另一个服务。比较典型的就是根据当前承载环境(开发、预发和产品)对某个Options对象做动态设置。为了解决这个问题,Options模型提供了一个ConfigureNamedOptions<TOptions, TDep>,其中,第二个反省参数代表依赖的服务类型。如下面的代码片段所示,ConfigureNamedOptions<TOptions, TDep>依然是IConfigureNamedOptions<TOptions>接口的实现类型,它利用Action<TOptions, TDep>对象针对指定的依赖服务对Options做针对性初始化。
public class ConfigureNamedOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions> where TOptions : class where TDep : class { public string Name { get; } public Action<TOptions, TDep> Action { get; } public TDep Dependency { get; } public ConfigureNamedOptions(string name, TDep dependency, Action<TOptions, TDep> action) { Name = name; Action = action; Dependency = dependency; } public virtual void Configure(string name, TOptions options) { if (Name == null || name == Name) { Action?.Invoke(options, Dependency); } } public void Configure(TOptions options) => Configure(Options.DefaultName, options); }
ConfigureNamedOptions<TOptions, TDep>仅仅实现了针对单一服务的依赖,针对Options的初始化可能依赖多个服务,Options模型为此定义了如下所示的一系列类型。这些类型都实现了IConfigureNamedOptions<TOptions>接口,并采用类似于ConfigureNamedOptions<TOptions, TDep>类型的方式实现了Configure方法。
public class ConfigureNamedOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> : IConfigureNamedOptions<TOptions> where TOptions : class where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class { public string Name { get; } public TDep1 Dependency1 { get; } public TDep2 Dependency2 { get; } public TDep3 Dependency3 { get; } public TDep4 Dependency4 { get; } public TDep5 Dependency5 { get; } public Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> Action { get; } public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, TDep3 dependency3, TDep4 dependency4, TDep5 dependency5, Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> action); public void Configure(TOptions options); public virtual void Configure(string name, TOptions options); }
四、PostConfigureOptions<TOptions>
默认实现IPostConfigureOptions<TOptions>接口的是PostConfigureOptions<TOptions>类型。从给出的代码片段可以看出它针对Options对象的初始化实现方式与ConfigureNamedOptions<TOptions>类型并没有本质的差别。
public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class { public string Name { get; } public Action<TOptions> Action { get; } public PostConfigureOptions(string name, Action<TOptions> action) { Name = name; Action = action; } public void PostConfigure(string name, TOptions options) { if (Name == null || name == Name) { Action?.Invoke(options); } } }
Options模型同样定义了如下这一系列针对依赖服务的IPostConfigureOptions<TOptions>接口实现。如果针对Options对象的后置初始化操作依赖于其他服务,就可以根据服务的数量选择对应的类型。这些类型针对PostConfigure方法的实现与ConfigureNamedOptions<TOptions, TDep>类型实现Configure方法并没有本质区别。
- PostConfigureOptions<TOptions, TDep>。
- PostConfigureOptions<TOptions, TDep1, TDep2>。
- PostConfigureOptions<TOptions, TDep1, TDep2, TDep3>。
- PostConfigureOptions<TOptions, TDep1, TDep2, TDep3, TDep4>。
- PostConfigureOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5>。
五、ValidateOptions<TOptions>
ValidateOptions<TOptions>是对IValidateOptions<TOptions>接口的默认实现。如下面的代码片段所示,创建一个ValidateOptions<TOptions>对象时,需要提供Options的名称和验证错误消息,以及真正用于对Options进行验证的Func<TOptions, bool>对象。
public class ValidateOptions<TOptions> : IValidateOptions<TOptions>where TOptions : class { public string Name { get; } public string FailureMessage { get; } public Func<TOptions, bool> Validation { get; } public ValidateOptions(string name, Func<TOptions, bool> validation, string failureMessage); public ValidateOptionsResult Validate(string name, TOptions options); }
对Options的验证同样可能具有对其他服务的依赖,比较典型的依然是针对不同的承载环境(开发、预发和产品)具有不同的验证规则,所以IValidateOptions<TOptions>接口同样具有如下5个针对不同依赖服务数量的实现类型。
- ValidateOptions<TOptions, TDep>
- ValidateOptions<TOptions, TDep1, TDep2>
- ValidateOptions<TOptions, TDep1, TDep2, TDep3>
- ValidateOptions<TOptions, TDep1, TDep2, TDep3, TDep4>
- ValidateOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5>
前面介绍了OptionsFactory<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]: 与配置系统的整合