[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]
通过前面演示的几个实例(配置选项的正确使用方式[上篇]、配置选项的正确使用方式[下篇]),我们已经对基于Options的编程方式有了一定程度的了解,下面从设计的角度介绍Options模型。我们演示的实例已经涉及Options模型的3个重要的接口,它们分别是IOptions
通过前面演示的几个实例(配置选项的正确使用方式[上篇]、配置选项的正确使用方式[下篇]),我们已经对基于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
。 - PostConfigureOptions
。 - PostConfigureOptions
。 - PostConfigureOptions
。 - PostConfigureOptions
。
五、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
- ValidateOptions
- ValidateOptions
- ValidateOptions
- ValidateOptions
前面介绍了OptionsFactory<TOptions>类型针对Options对象的创建和初始化的实现原理,以及涉及的一些相关的接口和类型,下图基本上反映了这些接口与类型的关系。
作者:蒋金楠微信公众账号:大内老A微博:www.weibo.com/artech如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文:https://www.cnblogs.com/artech/p/inside-asp-net-core-06-03.html