前两篇介绍的都是已IConfiguration为基础的配置,这里在说说.net core提供的一种全新的辅助配置机制:Options。

  Options,翻译成中文就是选项,可选择的意思,它依赖于.net core提供的DI机制(DI机制以后再说),Options的对象是具有空构造函数的类

  Options是一个独立的拓展库,它不像IConfiguration那样可以从外部文件获取配置,它其实可以理解为一种代码层面的配置,.net core内部大量的实现类采用了IOptions机制,基本上,.net core中任何一个依赖DI存在的库,或多或少都会有Options的影子,比如日志的LoggerFilterOptions,认证授权的AuthenticationOptions等等,

  

  一、原理

  想了一下,这里原理的介绍可以分成两个部分:配置和读取

  配置

  Options的配置一般采用IServiceCollection的Configure,ConfigureAll,PostConfigure,PostConfigureAll,ConfigureOptions和带泛型参数的AddOptions<TOptions>等拓展方法以及他们的重载来实现,同时,Options可以指定一个名称,用来区分同一类型的Options,如果不指定名称,那么默认将采用Options.DefaultName(源码)作为名称,其实也就是空字符串(不是null,当名称是null时代表全部,后面介绍)。其实这几个方法的本质就是往DI容器中注册IConfigureOptions<TOptions>(源码)或者IPostConfigureOptions<TOptions>(源码)接口的服务,只不过注册进去的类或者名称不一样而已,可以查看源码(源码)。

  Configure和ConfigureAll

  Configure和ConfigureAll是最主要的配置入口,对同一个类型可以多次进行配置,其中,Configure是对指定名称的Options进行配置,而ConfigureAll是对同一类型的所有Options进行配置,其实ConfigureAll(action)等价于Configure(null,action),这里是前面说的Options的默认名称不是null,而是空字符串(源码):  

    public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
           => services.Configure(name: null, configureOptions: configureOptions);

  所以我们只需要关注Configure方法就可以了,Configure注册的服务是ConfigureNamedOptions<TOptions>源码),它实现了IConfigureNamedOptions<TOptions>接口,而IConfigureNamedOptions<TOptions>接口是IConfigureOptions<TOptions>接口的一个子接口,接口实现内容如下(源码):  

   // IConfigureNamedOptions<TOptions>接口实现
public virtual void Configure(string name, TOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to configure all named options. if (Name == null || name == Name) { Action?.Invoke(options); } } public void Configure(TOptions options) => Configure(Options.DefaultName, options);// IConfigureOptions<TOptions>接口实现

  从实现方法也可以看到,当Options的名称为null时,表示对所有此类型的Options均进行配置。

  总之,我们只需要记住,Configure和ConfigureAll方法只是往DI中对IConfigureOptions<TOptions>接口注册ConfigureNamedOptions<TOptions>服务,只不过ConfigureAll注册的名称是null,Configure注册的名称默认是Options.DefaultName。

  PostConfigure和PostConfigureAll

  有了Configure和ConfigureAll,为什么还要有PostConfigure和PostConfigureAll?举个例子,我们要组装车子,Configure1配置好了轮子,Configure2配置好了车架,Configure3配置好了内饰,那组装要等这三个配置好了才能组装吧,这也就是PostConfigure的由来。

  和ConfigureAll一样,PostConfigureAll与PostConfigure的区别就是PostConfigureAll使用的name是null(源码):  

    public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
         => services.PostConfigure(name: null, configureOptions: configureOptions);

  所以,我们也只需要关注PostConfigure方法就可以了,而PostConfigure方法注册的服务是PostConfigureOptions<TOptions>源码),它实现的是IPostConfigureOptions<TOptions>接口(源码):  

    public virtual void PostConfigure(string name, TOptions options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        // Null name is used to initialize all named options.
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }

  实现内容几乎和ConfigureNamedOptions<TOptions>是一样的,总之,只需要记住,PostConfigure和PostConfigureAll方法只是往DI中对IPostConfigureOptions<TOptions>接口注册PostConfigureOptions<TOptions>服务,只不过PostConfigureAll注册的名称是null,PostConfigure注册的名称默认是Options.DefaultName。

  ConfigureOptions

  前面说到,无论是Configure还是PostConfigure,都是往DI容器中注册IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的服务,但是他们配置的载体是委托Action,因此,ConfigureOptions方法允许我们自己以类的形式作为载体去进行配置,只不过需要我们自己去实现IConfigureOptions<TOptions>或IPostConfigureOptions<TOptions>接口,或者我们也可以使用默认实现好了的几个Options:ConfigureOptions<TOptions>、ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,如果自己实现,比如有实现类:  

    public class TestConfigureOptions : IConfigureOptions<TestOptions>, IPostConfigureOptions<TestOptions>
    {
        public void Configure(TestOptions options)
        {
            //配置
        }

        public void PostConfigure(string name, TestOptions options)
        {
            //配置
        }
    }
    public class TestOptions
    {
        //属性
    }

  然后可以使用ConfigureOptions方法配置了:  

    public void ConfigureServices(IServiceCollection services)
    {
        services.ConfigureOptions<TestConfigureOptions>();
        ...
    }

  AddOptions<TOptions>

  带泛型的AddOptions<TOptions>方法返回一个OptionsBuilder<TOptions>方法(源码),它则可进行更多的配置,比如上面Configure和PostConfigure方法的功能,但是OptionsBuilder<TOptions>只是配置包含名称的Options,默认名称就是Options.DefaultName,也就是说OptionsBuilder<TOptions>无法配置像ConfigureAll和PostConfigureAll那样的功能。

  OptionsBuilder<TOptions>除了包含Configure和PostConfigure方法的功能,主要还有几个功能:

  1、OptionsBuilder<TOptions>允许我们从DI中获取服务或者其他配置来进行操作进一步的配置,比如我们有下面的Options:  

    public class VarOptions
    {
        public int Var { get; set; }
    }
    public class SumOptions
    {
        public int Sum { get; set; }
    }
    public class MultipleOptions
    {
        public int Multiple { get; set; }
    }

  然后我们使用配置:  

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<VarOptions>("Var1", options =>
        {
            options.Var = 1;
        });
        services.Configure<VarOptions>("Var2", options =>
        {
            options.Var = 2;
        });
        services.AddOptions<SumOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) =>
        {
            var varOption1 = factory.Create("Var1");
            var varOption2 = factory.Create("Var2");
            options.Sum = varOption1.Var + varOption2.Var;
        });
        services.AddOptions<MultipleOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) =>
        {
            var varOption1 = factory.Create("Var1");
            var varOption2 = factory.Create("Var2");
            options.Multiple = varOption1.Var * varOption2.Var;
        });
        ...
    }

  可以看到,VarOptions有两个名称:Var1和Var2,我们的SumOptions和MultipleOptions的配置是从DI中获取VarOptions的配置来生成的。

  注意的是,OptionsBuilder<TOptions>的Configure和PostConfigure方法往DI中注册的服务也不一样,除了ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,还会有很多ConfigureNamedOptions<TOptions,TDep1,TDep2...>和PostConfigureOptions<TOptions,TDep1,TDep2...>这样的服务实现类。

  2、OptionsBuilder<TOptions>提供了Validate方法及它的重载,允许我们配置完Options后,可以自定义的对Options进行验证,比如上面我们将SumOptions增加验证,要求相加后的值要大于10:  

    services.AddOptions<SumOptions>().Configure<IOptionsFactory<VarOptions>>((options, factory) =>
    {
        var varOption1 = factory.Create("Var1");
        var varOption2 = factory.Create("Var2");
        options.Sum = varOption1.Var + varOption2.Var;
    }).Validate(options => options.Sum > 10);

  这样,当配置完SumOptions之后,在验证时,发现它的Sum属性不大于10,那么就会抛出异常了。

  注意,这个验证是在获取配置使用的时候进行的

  本质上,OptionsBuilder<TOptions>的Validate方法其实是往DI中注册IValidateOptions<TOptions>接口的服务:ValidateOptions<TOptions>和很多ValidateOptions<TOptions,TDep1,TDep2...>。

  3、OptionsBuilder<TOptions>可以给Options增加特性验证,熟悉EF的朋友肯定都知道,我们可以是实体的属性增加一些特性,比如RequiredAttribute,MaxLengthAttribute等,然后EF就是自动帮我们进行验证了,同样的,我们也可以对Options使用这些特性,比如,我们有下面的一个Options:  

    public class TestOptions
    {
        [Required, MaxLength(5)]
        public string Value { get; set; }
    }

  然后做下面的配置:  

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<TestOptions>("Test1").Configure(options =>
        {
            options.Value = null;
        }).ValidateDataAnnotations();
        services.AddOptions<TestOptions>("Test2").Configure(options =>
       {
           options.Value = "1234567890";
       }).ValidateDataAnnotations();
        services.AddOptions<TestOptions>("Test3").Configure(options =>
       {
           options.Value = "abc";
       }).ValidateDataAnnotations();
        ...
    }

  当我们获取名称是Test1的Options是会因为Required特性报错,当我们获取名称是Test2的Options时,会因为MaxLength(5)报错,而Test3是正确的。

  另外,可以看到,这里验证只是使用了ValidateDataAnnotations方法(源码),其实它只是Options验证的一个拓展,它只不过是使用了DataAnnotationValidateOptions<TOptions>(源码)来做验证,而DataAnnotationValidateOptions<TOptions>就是实现了 IValidateOptions<TOptions>接口的一个类:  

    public static OptionsBuilder<TOptions> ValidateDataAnnotations<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
    {
        optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(new DataAnnotationValidateOptions<TOptions>(optionsBuilder.Name));
        return optionsBuilder;
    }

  读取

  Options的配置说完了,再看看读取。

  无论是在配置的Configure,PostConfigure,还是ConfigureOptins,AddOptions<TOptions>方法,都是执行一个不带泛型参数的AddOptions方法(源码):  

    public static IServiceCollection AddOptions(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }

        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
        services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
        return services;
    }

  可以看到,这个方法就是注册5个类,它们就和Options读取有关,我们可以在服务(比如控制器)的构造函数中注入Options,比如:  

    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        public HomeController(IOptions<TestOptions> options,
            IOptionsFactory<TestOptions> optionsFactory,
            IOptionsMonitor<TestOptions> optionsMonitor,
            IOptionsSnapshot<TestOptions> optionsSnapshot,
            IOptionsMonitorCache<TestOptions> optionsMonitorCache)
        {
            var options1 = options.Value;
            var options2 = optionsFactory.Create(Options.DefaultName);
            var options3 = optionsMonitor.CurrentValue;//或者使用optionsMonitor.Get(name)
            var options4 = optionsSnapshot.Get(Options.DefaultName);
            var options5 = optionsMonitorCache.GetOrAdd(Options.DefaultName, () => new TestOptions());
        }
        ...
    }

  但是这五种方式的表现不一样:  

    IOptions<TOptions>:全局缓存配置(Singleton),也就是说Configure和PostConfigure等方法的配置内容只会被执行一遍,然后全局使用这一个配置
    IOptionsSnapshot<TOptions>:范围内的配置(Scoped,这个以后DI中说,暂时可以认为一个http请求响应就是一个Scoped),也就是说一个Scoped范围内,Configure和PostConfigure等方法的配置内容只会被执行一遍
    IOptionsMonitor<TOptions>:全局可监听的配置(Singleton),首先从IOptionsMonitorCache<TOptions>缓存加载,没有加载到则使用IOptionsFactory<TOptions>创建,同时我们可以注册IOptionsChangeTokenSource<TOptions>来进行监听,决定何时清除缓存然后重新创建Options
    IOptionsFactory<TOptions>:Options的创建工厂(Singleton),他没有缓存,直接创建Options,这样从某种层面来说有性能的损失。
    IOptionsMonitorCache<TOptions>:IOptionsMonitor<TOptions>的缓存(Singleton),如果需要,我们可以直接从DI中获取缓存操作,来决定IOptionsMonitor<TOptions>接下来是从缓存中获取Options还是使用IOptionsFactory<TOptions>创建

  另外它们的实现类也有区别:

  1、IOptions<TOptions>和IOptionsSnapshot<TOptions>都是采用OptionsManager<TOptions>(源码),它的源码很简单,实际上就是从DI中获取IOptionsFactory<TOptions>工厂来创建Options

  2、IOptionsMonitorCache<TOptions>的服务类是OptionsCache<TOptions>(源码),它其实就是管理Options集合的类,比如增加,移除,清空等等。

  3、IOptionsFactory<TOptions>的服务类是OptionsFactory<TOptions>(源码),它从DI中获取TOptions的所有IConfigureOptions<TOptions>、IPostConfigureOptions<TOptions>和 IValidateOptions<TOptions>的服务类,可以看看它的Create方法(源码):  

    public TOptions Create(string name)
    {
        var options = new TOptions();
        foreach (var setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        foreach (var post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        if (_validations != null)
        {
            var failures = new List<string>();
            foreach (var validate in _validations)
            {
                var result = validate.Validate(name, options);
                if (result.Failed)
                {
                    failures.AddRange(result.Failures);
                }
            }
            if (failures.Count > 0)
            {
                throw new OptionsValidationException(name, typeof(TOptions), failures);
            }
        }

        return options;
    }

  现在,上面不断介绍的往DI中注册的IConfigureOptions<TOptions>、IPostConfigureOptions<TOptions>和 IValidateOptions<TOptions>知道在哪里用,怎么用的了吧。

  4、IOptionsMonitor<TOptions>的服务类是OptionsMonitor<TOptions>(源码),它注入IOptionsFactory<TOptions>,IOptionsMonitorCache<TOptions>,还有所有的IOptionsChangeTokenSource<TOptions>,它会优先从IOptionsMonitorCache<TOptions>缓存中获取Options,如果缓存没有,则使用IOptionsFactory<TOptions>创建并放入缓存中,而IOptionsChangeTokenSource<TOptions>是IOptionsMonitor<TOptions>的监听机制,它决定了IOptionsMonitorCache<TOptions>何时刷新,从而可以让IOptionsFactory<TOptions>去创建。

  

  二、Options和IConfiguration

  Options和IConfiguration是可以结合使用的,IConfiguration从外部读取配置,然后使用Options将配置读取到我们熟悉的实体中使用,还可以和IConfiguration的重新加载机制结合。

  .net core中通过拓展IServiceCollection的Configure方法(源码)和OptionsBuilder<TOptions>的Bind方法(源码)来集合IConfiguration,不过最终都是同下面的Configure方法进行注册(源码):

    public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
        where TOptions : class
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }

        if (config == null)
        {
            throw new ArgumentNullException(nameof(config));
        }

        services.AddOptions();
        services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
        return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
    }

  可以看到,它注册的是IConfigureOptions<TOptions>接口的NamedConfigureFromConfigurationOptions<TOptions>(源码)服务,而NamedConfigureFromConfigurationOptions<TOptions>只是ConfigureNamedOptions<TOptions>的一个子类,只不过NamedConfigureFromConfigurationOptions<TOptions>中是将IConfiguration中的配置值通过它的Bind拓展方法绑定到实体Options上。

  另外,这里面还注册了IOptionsChangeTokenSource<TOptions>的服务ConfigurationChangeTokenSource<TOptions>(源码),它的作用就是将Options的监听与IConfiguration的重新加载机制结合起来。

  在使用时,举个例子,比如appsettings.json有如下配置

  {
    ...
    "Data": {
      "Value1": 1,
      "Value2": 3.14,
      "Value3": true,
      "Value4": [ 1, 2, 3 ],
      "Value5": {
        "Value1": 2,
        "Value2": 5.20,
        "Value3": false,
        "Value4": [ 4,5,6,7 ]
      }
    }
  }

  然后我们有一个对应的ptions

    public class DataOptions
    {
        public int Value1 { get; set; }
        public decimal Value2 { get; set; }
        public bool Value3 { get; set; }
        public int[] Value4 { get; set; }
        public DataOptions Value5 { get; set; }
    }

  然后只需要结合IConfiguration和Options注册即可:  

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<DataOptions>(Configuration.GetSection("Data"));
        ...
    }

  接下来就可以直接以Options的方式读取配置了

  

  三、Options使用例子

  下面例子的Demo已上传:https://pan.baidu.com/s/10mU79U6YYCj4-yQies6zRQ (提取码: yywq )

  更多集成使用的Demo可以参考这里我封装实现的.net core对RabbitMQ,ActiveMQ,Kafka等操作的Demo:https://gitee.com/shanfeng1000/dotnetcore-demo

  不带名称的Options

  不带名称的Options常用于一些全局的配置,比如MvcOptions,或者一些创建工厂的配置Options,也就是说往往我们的DI中只存在一个服务类或者不用区分服务类的时候,往往使用的是不带名称的Options。

  举个例子,比如我们有下面的连接工厂类及连接类:

  
    public interface IConnectionFactory
    {
        /// <summary>
        /// 创建连接
        /// </summary>
        /// <returns></returns>
        IConnection Create();
    }
    public class ConnectionFactory : IConnectionFactory
    {
        IOptionsMonitor<ConnectionFactoryOptions> optionsMonitor;
        public ConnectionFactory(IOptionsMonitor<ConnectionFactoryOptions> optionsMonitor)
        {
            this.optionsMonitor = optionsMonitor;
        }

        /// <summary>
        /// 创建连接
        /// </summary>
        /// <returns></returns>
        public IConnection Create()
        {
            return new Connection(optionsMonitor.CurrentValue.ConnectionString);
        }
    }
    public class ConnectionFactoryOptions
    {
        /// <summary>
        /// 连接字符串
        /// </summary>
        public string ConnectionString { get; set; }
    }
    public interface IConnection
    {
        /// <summary>
        /// 打开连接
        /// </summary>
        void Open();
        /// <summary>
        /// 关闭连接
        /// </summary>
        void Close();
    }
    public class Connection : IConnection
    {
        string connectionString;
        public Connection(string connectionString)
        {
            this.connectionString = connectionString;
        }

        /// <summary>
        /// 打开连接
        /// </summary>
        public void Open()
        {
            Console.WriteLine("Connecting:" + connectionString);
            Console.WriteLine("Connection Opened!");
        }
        /// <summary>
        /// 关闭连接
        /// </summary>
        public void Close()
        {
            Console.WriteLine("Disconnecting:" + connectionString);
            Console.WriteLine("Connection Closed!");
        }
    }
ConnectionFactory

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2018-09-17
  • 2021-07-19
  • 2022-03-03
  • 2021-09-07
  • 2021-10-29
  • 2022-01-19
猜你喜欢
  • 2021-07-31
  • 2021-10-18
  • 2022-12-23
  • 2021-11-03
  • 2022-01-31
相关资源
相似解决方案