zoukankan      html  css  js  c++  java
  • [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]: 与配置系统的整合

  • 相关阅读:
    SSL JudgeOnline 1194——最佳乘车
    SSL JudgeOnline 1457——翻币问题
    SSL JudgeOnlie 2324——细胞问题
    SSL JudgeOnline 1456——骑士旅行
    SSL JudgeOnline 1455——电子老鼠闯迷宫
    SSL JudgeOnline 2253——新型计算器
    SSL JudgeOnline 1198——求逆序对数
    SSL JudgeOnline 1099——USACO 1.4 母亲的牛奶
    SSL JudgeOnline 1668——小车载人问题
    SSL JudgeOnline 1089——USACO 1.2 方块转换
  • 原文地址:https://www.cnblogs.com/artech/p/inside-asp-net-core-06-03.html
Copyright © 2011-2022 走看看