[ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇]

Options[3]: Options模型[上篇] - 图1通过前面演示的几个实例(配置选项的正确使用方式[上篇]、配置选项的正确使用方式[下篇]),我们已经对基于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>相关的接口和类型主要体现在下图中。

7-7

下面以上图为基础介绍OptionsManager<TOptions>对象是如何提供Options对象的。如下面的代码片段所示,IOptions<TOptions>接口和IOptionsSnapshot<TOptions>接口的泛型参数的TOptions类型要求具有一个默认的构造函数,也就是说,Options对象可以在无须指定参数的情况下直接采用new关键字进行实例化,实际上,Options最初就是采用这种方式创建的。

  1. public interface IOptions<out TOptions> where TOptions: class, new()
  2. {
  3. TOptions Value { get; }
  4. }
  5.  
  6. public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions> where TOptions: class, new()
  7. {
  8. TOptions Get(string name);
  9. }

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>类型是如何定义的。

  1. public class OptionsManager<TOptions> :IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
  2. {
  3. private readonly IOptionsFactory<TOptions> _factory;
  4. private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>();
  5.  
  6. public OptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory;
  7. public TOptions Value => this.Get(Options.DefaultName);
  8. public TOptions Get(string name) => _cache.GetOrAdd(name, () => _factory.Create(name));
  9. }
  10.  
  11. public static class Options
  12. {
  13. public static readonly string DefaultName = string.Empty;
  14. }

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对象。

  1. public interface IOptionsFactory<TOptions> where TOptions: class, new()
  2. {
  3. TOptions Create(string name);
  4. }

OptionsFactory<TOptions>OptionsFactory<TOptions>是IOptionsFactory<TOptions>接口的默认实现。OptionsFactory<TOptions>对象针对Options对象的创建主要分3个步骤来完成,笔者将这3个步骤称为Options对象相关的“实例化”、“初始化”和“验证”。由于Options类型总是具有一个公共默认的构造函数,所以OptionsFactory<TOptions>的实现只需要利用new关键字调用这个构造函数就可以创建一个空的Options对象。当Options对象被实例化之后,OptionsFactory<TOptions>对象会根据注册的一些服务对其进行初始化。Options模型中针对Options对象初始化的工作由如下3个接口表示的服务负责。

  1. public interface IConfigureOptions<in TOptions> where TOptions: class
  2. {
  3. void Configure(TOptions options);
  4. }
  5.  
  6. public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
  7. {
  8. void Configure(string name, TOptions options);
  9. }
  10.  
  11. public interface IPostConfigureOptions<in TOptions> where TOptions : class
  12. {
  13. void PostConfigure(string name, TOptions options);
  14. }

上述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的名称。

  1. public interface IValidateOptions<TOptions> where TOptions : class
  2. {
  3. ValidateOptionsResult Validate(string name, TOptions options);
  4. }
  5.  
  6. public class ValidateOptionsResult
  7. {
  8. public static readonly ValidateOptionsResult Success;
  9. public static readonly ValidateOptionsResult Skip;
  10. public static ValidateOptionsResult Fail(string failureMessage);
  11.  
  12. public bool Succeeded { get; protected set; }
  13. public bool Skipped { get; protected set; }
  14. public bool Failed { get; protected set; }
  15. public string FailureMessage { get; protected set; }
  16. }

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>类型的完整定义。

  1. public class OptionsFactory<TOptions> :IOptionsFactory<TOptions> where TOptions : class, new()
  2. {
  3. private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
  4. private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
  5. private readonly IEnumerable<IValidateOptions<TOptions>> _validations;
  6.  
  7. public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
  8. : this(setups, postConfigures, null)
  9. { }
  10.  
  11. public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
  12. {
  13. _setups = setups;
  14. _postConfigures = postConfigures;
  15. _validations = validations;
  16. }
  17.  
  18. public TOptions Create(string name)
  19. {
  20. //步骤1:实例化
  21. var options = new TOptions();
  22.  
  23. //步骤2-1:针对IConfigureNamedOptions<TOptions>的初始化
  24. foreach (var setup in _setups)
  25. {
  26. if (setup is IConfigureNamedOptions<TOptions> namedSetup)
  27. {
  28. namedSetup.Configure(name, options);
  29. }
  30. else if (name == Options.DefaultName)
  31. {
  32. setup.Configure(options);
  33. }
  34. }
  35.  
  36. //步骤2-2:针对IPostConfigureOptions<TOptions>的初始化
  37. foreach (var post in _postConfigures)
  38. {
  39. post.PostConfigure(name, options);
  40. }
  41.  
  42. //步骤3:有效性验证
  43. var failedMessages = new List<string>();
  44. foreach (var validator in _validations)
  45. {
  46. var reusult = validator.Validate(name, options);
  47. if (reusult.Failed)
  48. {
  49. failedMessages.Add(reusult.FailureMessage);
  50. }
  51. }
  52. if (failedMessages.Count > 0)
  53. {
  54. throw new OptionsValidationException(name, typeof(TOptions),
  55. failedMessages);
  56. }
  57. return options;
  58. }
  59. }

如上面的代码片段所示,调用构造函数创建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对象的初始化。

7-8

三、ConfigureNamedOptions<TOptions>

对于上述3个用来初始化Options对象的接口,Options模型均提供了默认实现,其中,ConfigureNamedOptions<TOptions>类同时实现了IConfigureOptions<TOptions>和IConfigureNamedOptions<TOptions>接口。当我们创建这样一个对象时,需要指定Options的名称和一个用来初始化Options对象的Action<TOptions>委托对象。如果指定了一个非空的名称,那么提供的委托对象将会用于初始化与该名称相匹配的Options对象;如果指定的名称为Null(不是空字符串),就意味着提供的初始化操作适用于所有同类的Options对象。

  1. public class ConfigureNamedOptions<TOptions> :IConfigureNamedOptions<TOptions>,IConfigureOptions<TOptions> where TOptions : class
  2. {
  3. public string Name { get; }
  4. public Action<TOptions> Action { get; }
  5.  
  6. public ConfigureNamedOptions(string name, Action<TOptions> action)
  7. {
  8. Name = name;
  9. Action = action;
  10. }
  11.  
  12. public void Configure(string name, TOptions options)
  13. {
  14. if (Name == null || name == Name)
  15. {
  16. Action?.Invoke(options);
  17. }
  18. }
  19.  
  20. public void Configure(TOptions options) => Configure(Options.DefaultName, options);
  21. }

有时针对某个Options的初始化工作需要依赖另一个服务。比较典型的就是根据当前承载环境(开发、预发和产品)对某个Options对象做动态设置。为了解决这个问题,Options模型提供了一个ConfigureNamedOptions<TOptions, TDep>,其中,第二个反省参数代表依赖的服务类型。如下面的代码片段所示,ConfigureNamedOptions<TOptions, TDep>依然是IConfigureNamedOptions<TOptions>接口的实现类型,它利用Action<TOptions, TDep>对象针对指定的依赖服务对Options做针对性初始化。

  1. public class ConfigureNamedOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions>
  2. where TOptions : class
  3. where TDep : class
  4. {
  5. public string Name { get; }
  6. public Action<TOptions, TDep> Action { get; }
  7. public TDep Dependency { get; }
  8.  
  9. public ConfigureNamedOptions(string name, TDep dependency, Action<TOptions, TDep> action)
  10. {
  11. Name = name;
  12. Action = action;
  13. Dependency = dependency;
  14. }
  15.  
  16. public virtual void Configure(string name, TOptions options)
  17. {
  18. if (Name == null || name == Name)
  19. {
  20. Action?.Invoke(options, Dependency);
  21. }
  22. }
  23.  
  24. public void Configure(TOptions options) => Configure(Options.DefaultName, options);
  25. }

ConfigureNamedOptions<TOptions, TDep>仅仅实现了针对单一服务的依赖,针对Options的初始化可能依赖多个服务,Options模型为此定义了如下所示的一系列类型。这些类型都实现了IConfigureNamedOptions<TOptions>接口,并采用类似于ConfigureNamedOptions<TOptions, TDep>类型的方式实现了Configure方法。

  1. public class ConfigureNamedOptions<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> : IConfigureNamedOptions<TOptions>
  2. where TOptions : class
  3. where TDep1 : class
  4. where TDep2 : class
  5. where TDep3 : class
  6. where TDep4 : class
  7. where TDep5 : class
  8. {
  9. public string Name { get; }
  10. public TDep1 Dependency1 { get; }
  11. public TDep2 Dependency2 { get; }
  12. public TDep3 Dependency3 { get; }
  13. public TDep4 Dependency4 { get; }
  14. public TDep5 Dependency5 { get; }
  15. public Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> Action { get; }
  16.  
  17. public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, TDep3 dependency3, TDep4 dependency4, TDep5 dependency5, Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> action);
  18. public void Configure(TOptions options);
  19. public virtual void Configure(string name, TOptions options);
  20. }

四、PostConfigureOptions<TOptions>

默认实现IPostConfigureOptions<TOptions>接口的是PostConfigureOptions<TOptions>类型。从给出的代码片段可以看出它针对Options对象的初始化实现方式与ConfigureNamedOptions<TOptions>类型并没有本质的差别。

  1. public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
  2. {
  3. public string Name { get; }
  4. public Action<TOptions> Action { get; }
  5.  
  6. public PostConfigureOptions(string name, Action<TOptions> action)
  7. {
  8. Name = name;
  9. Action = action;
  10. }
  11.  
  12. public void PostConfigure(string name, TOptions options)
  13. {
  14. if (Name == null || name == Name)
  15. {
  16. Action?.Invoke(options);
  17. }
  18. }
  19. }

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>对象。

  1. public class ValidateOptions<TOptions> : IValidateOptions<TOptions>where TOptions : class
  2. {
  3. public string Name { get; }
  4. public string FailureMessage { get; }
  5. public Func<TOptions, bool> Validation { get; }
  6. public ValidateOptions(string name, Func<TOptions, bool> validation, string failureMessage);
  7. public ValidateOptionsResult Validate(string name, TOptions options);
  8. }

对Options的验证同样可能具有对其他服务的依赖,比较典型的依然是针对不同的承载环境(开发、预发和产品)具有不同的验证规则,所以IValidateOptions<TOptions>接口同样具有如下5个针对不同依赖服务数量的实现类型。

  • ValidateOptions
  • ValidateOptions
  • ValidateOptions
  • ValidateOptions
  • ValidateOptions前面介绍了OptionsFactory<TOptions>类型针对Options对象的创建和初始化的实现原理,以及涉及的一些相关的接口和类型,下图基本上反映了这些接口与类型的关系。

7-9

Options[3]: Options模型[上篇] - 图5

作者:蒋金楠微信公众账号:大内老A微博:www.weibo.com/artech如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号)。本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文:https://www.cnblogs.com/artech/p/inside-asp-net-core-06-03.html