[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>相关的接口和类型主要体现在下图中。

7-7

下面以上图为基础介绍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对象的初始化。

7-8

三、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对象的创建和初始化的实现原理,以及涉及的一些相关的接口和类型,下图基本上反映了这些接口与类型的关系。

7-9

[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]: 与配置系统的整合

posted @ 2020-01-13 08:51  Artech  阅读(...)  评论(...编辑  收藏