• .net core的配置介绍(三):Options


      前两篇介绍的都是已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

      注意到,我们.net core推荐面向接口开发,所以这里推荐使用了IConnectionFactory和IConnection接口。

      另一方面,这些类的服务注册我们可以直接写在Startup中,但是推荐拓展方法做一层封装,然后在Startup中使用services.AddXXXXX()的形式注册,比如这里我们实现拓展类:  

        public static class ConnectionFactoryExtensions
        {
            /// <summary>
            /// 添加连接
            /// </summary>
            /// <param name="services"></param>
            /// <param name="configuration"></param>
            /// <returns></returns>
            public static IServiceCollection AddConnectionFactory(this IServiceCollection services, IConfiguration configuration)
            {
                if (configuration == null) throw new ArgumentNullException(nameof(configuration));
    
                services.Configure<ConnectionFactoryOptions>(configuration);
                services.TryAddSingleton<IConnectionFactory, ConnectionFactory>();
                return services;
            }
        }

      注意到,这里一般使用TryAddSingleton而不是AddSingleton,这样可以避免重复注册服务,而且,当我们注册不带名称的Options时,优先考虑使用IConfiguration,如果我们的Options数据不是来自IConfiguration,则可使用Action<TOptions>来实现。

      假如我们在appsettings.json中有如下配置:  

      {
        ...
        "ConnectionFactoryOptions": {
          "ConnectionString": "Oracle ConnectionString"
        }
      }

      然后我们可以在Startup中这么写:  

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddConnectionFactory(Configuration.GetSection("ConnectionFactoryOptions"));
            ...
        }

      我们可以使用WebApi的接口Action做个Demo:  

        /// <summary>
        /// 不带名称的Connectin工厂测试
        /// </summary>
        /// <returns></returns>
        [HttpGet("Connection")]
        public object Connection()
        {
            var factory = HttpContext.RequestServices.GetService<IConnectionFactory>();
            var connection = factory.Create();
            connection.Open();
    
            //do something...
            Thread.Sleep(1000);
    
            connection.Close();
    
            return "success";
        }

      运行项目,然后调用接口,就可以看到控制台输出:

      

      保持项目处于运行状态,我们可以修改appsettings.json:

      {
        ...
        "ConnectionFactoryOptions": {
          "ConnectionString": "Mysql ConnectionString"
        }
      }

      然后重新调用接口,你会发现Options重新加载了,其实这本质就是IConfiguration重新加载了:

      

      带名称的Options

      有时候,我们往DI中注册的同一类型服务使用Options可能不一样,这种情况多数表现在Client模式下,这个时候就可以采用名称作为区分,比如.netcore 提供的AddAuthentication认证服务注册方法,可以注册多种认证方式,它们使用不同的名称做区分,不同名称的认证方式使用不同的配置,当我们要使用某个名称的认证时,一般只需要在Action中使用AuthorizeAttribute特性修饰,同时制定使用的认证名称即可。

      举个例子,比如我们有以下的Client和它的工厂:  

      
        public interface IClientFactory
        {
            /// <summary>
            /// 创建Client
            /// </summary>
            /// <param name="name"></param>
            /// <returns></returns>
            IClient Create(string name);
        }
        public class ClientFactory : IClientFactory
        {
            IOptionsMonitor<ClientOptions> optionsMonitor;
            public ClientFactory(IOptionsMonitor<ClientOptions> optionsMonitor)
            {
                this.optionsMonitor = optionsMonitor;
            }
    
            /// <summary>
            /// 创建Client
            /// </summary>
            /// <param name="name"></param>
            /// <returns></returns>
            public IClient Create(string name)
            {
                ClientOptions clientOptions = optionsMonitor.Get(name);
                return new Client(name, clientOptions);
            }
        }
        public class ClientOptions
        {
            /// <summary>
            /// 时间
            /// </summary>
            public DateTime Time { get; set; }
        }
        public interface IClient
        {
            /// <summary>
            /// Do something
            /// </summary>
            void Invoke();
        }
        public class Client : IClient
        {
            ClientOptions clientOptions;
            string name;
            public Client(string name, ClientOptions clientOptions)
            {
                this.name = name;
                this.clientOptions = clientOptions;
            }
    
            /// <summary>
            /// Do something
            /// </summary>
            public void Invoke()
            {
                Console.WriteLine($"{name}.Time:{clientOptions.Time:yyyy-MM-dd HH:mm:ss}");
            }
        }
    ClientFactory

      同样的,这里推荐使用面向接口开发,Startup中的注册推荐使用拓展方法封装:  

      
        public static class ClientFactoryExtensions
        {
            /// <summary>
            /// 添加Client
            /// </summary>
            /// <param name="services"></param>
            /// <param name="configure"></param>
            /// <returns></returns>
            public static IServiceCollection AddClientFactory(this IServiceCollection services, Action<ClientOptions> configure)
                => services.AddClientFactory(Options.DefaultName, configure);
            /// <summary>
            /// 添加Client
            /// </summary>
            /// <param name="services"></param>
            /// <param name="name"></param>
            /// <param name="configure"></param>
            /// <returns></returns>
            public static IServiceCollection AddClientFactory(this IServiceCollection services, string name, Action<ClientOptions> configure)
            {
                if (configure == null) throw new ArgumentNullException(nameof(configure));
    
                services.Configure(name, configure);
                services.TryAddSingleton<IClientFactory, ClientFactory>();
                return services;
            }
        }
    ClientFactoryExtensions

      往往,我们的Client配置不是从配置IConfiguration中读取的,所以一般使用Action<TOptions>作为配置载体,然后在Startup中使用:

        public void ConfigureServices(IServiceCollection services)
        {        
            services.AddClientFactory("Client1", options =>
            {
                options.Time = DateTime.Now;
            });
            services.AddClientFactory("Client2", options =>
            {
                options.Time = DateTime.Now;
            });
            ...
        }

      同样的,我们可以使用WebApi接口来说明使用方法:

         /// <summary>
         /// 带名称的Client工厂测试
         /// </summary>
         /// <param name="name"></param>
         /// <returns></returns>
         [HttpGet("Client")]
         public object Client(string name)
         {
             var factory = HttpContext.RequestServices.GetService<IClientFactory>();
             var client = factory.Create(name);
             client.Invoke();
             return "success";
         }
         /// <summary>
         /// 删除IOptionsMonitorCache中的缓存,可以触发重新创建Options
         /// </summary>
         /// <param name="name"></param>
         /// <returns></returns>
         [HttpGet("Refresh")]
         public object Refresh(string name)
         {
             var cache = HttpContext.RequestServices.GetService<IOptionsMonitorCache<ClientOptions>>();
             cache.TryRemove(name);
             Console.WriteLine("Refresh");
    return "success"; }

      运行起来后调用Client接口,控制台会输出:

      

      因为我们使用的是IOptionsMonitor<TOptions>,它是有缓存存在的,因此每次创建的Options都是一样的,我们可以使用IOptionsMonitorCache<TOptions>来删除缓存,比如上面的Refresh接口:

      

      前面说到,除了使用IOptionsMonitorCache<TOptions>来删除缓存,还可以同过注册IOptionsChangeTokenSource<TOptions>接口的服务来实现,比如这里我们可以添加它的一个通用实现类和拓展方法:  

      
        public interface ICommonOptionsChangeTokenSource
        {
            /// <summary>
            /// 触发
            /// </summary>
            void Change();
        }
        public class CommonOptionsChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>, ICommonOptionsChangeTokenSource
        {
            CancellationTokenSource cancellationTokenSource;
            CancellationChangeToken cancellationChangeToken;
    
            public CommonOptionsChangeTokenSource(string name)
            {
                Name = name ?? Options.DefaultName;
                cancellationTokenSource = new CancellationTokenSource();
                cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token);
            }
    
            public string Name { get; }
    
            public IChangeToken GetChangeToken()
            {
                return cancellationChangeToken;
            }
            public void Change()
            {
                var _cancellationTokenSource = new CancellationTokenSource();
                Interlocked.Exchange(ref cancellationChangeToken, new CancellationChangeToken(_cancellationTokenSource.Token));
                Interlocked.Exchange(ref cancellationTokenSource, _cancellationTokenSource).Cancel();
            }
        }
    CommonOptionsChangeTokenSource
      
        public static class CommonOptionsChangeTokenSourceExtensions
        {
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="services"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action)
                => services.AddOptionsChangeTokenSource<TOptions>(Options.DefaultName, action);
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="services"></param>
            /// <param name="name"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, string name, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action)
            {
                if (action == null) throw new ArgumentNullException(nameof(action));
    
                return services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(serviceProvider =>
                {
                    var source = new CommonOptionsChangeTokenSource<TOptions>(name);
                    action?.Invoke(serviceProvider, source);
                    return source;
                });
            }
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="builder"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static OptionsBuilder<TOptions> AddOptionsChangeTokenSource<TOptions>(this OptionsBuilder<TOptions> builder, Action<IServiceProvider, ICommonOptionsChangeTokenSource> action)
                where TOptions : class
            {
                builder.Services.AddOptionsChangeTokenSource<TOptions>(builder.Name, action);
                return builder;
            }
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="services"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, Action<ICommonOptionsChangeTokenSource> action)
                => services.AddOptionsChangeTokenSource<TOptions>(Options.DefaultName, action);
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="services"></param>
            /// <param name="name"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static IServiceCollection AddOptionsChangeTokenSource<TOptions>(this IServiceCollection services, string name, Action<ICommonOptionsChangeTokenSource> action)
            {
                if (action == null) throw new ArgumentNullException(nameof(action));
                var source = new CommonOptionsChangeTokenSource<TOptions>(name);
                action?.Invoke(source);
                return services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(source);
            }
            /// <summary>
            /// 添加IOptionsChangeTokenSource
            /// </summary>
            /// <typeparam name="TOptions"></typeparam>
            /// <param name="builder"></param>
            /// <param name="action"></param>
            /// <returns></returns>
            public static OptionsBuilder<TOptions> AddOptionsChangeTokenSource<TOptions>(this OptionsBuilder<TOptions> builder, Action<ICommonOptionsChangeTokenSource> action)
                where TOptions : class
            {
                builder.Services.AddOptionsChangeTokenSource<TOptions>(builder.Name, action);
                return builder;
            }
        }
    CommonOptionsChangeTokenSourceExtensions

      然后在Startup中使用:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptionsChangeTokenSource<ClientOptions>("Client1", source =>
            {
                //使用定时器来模拟触发重新创建Options
                System.Timers.Timer timer = new System.Timers.Timer();
                timer.Elapsed += (s, e) =>
                {
                    source.Change();
                };
                timer.Interval = 3000;//3秒更新一次
                timer.Start();
            });
            ...
        }

      这里采用定时器模拟,真实环境可能是采用一条消息总线或者是消息队列的通知来实现。

      这里为名称是Client1的Client添加定时刷新Options缓存的机制,而Client2不变,当运行项目后,再次调用Cient接口,会发现Client1的Time每个3秒刷新一次,而Client2则不变:

      

      

      四、总结

      有关Options的内容就说完了,把它和IConfiguration结合起来是一种非常好的配置形式,这也是.net core开发的基础,上面的例子也比较清楚,应该都能理解吧。

    一个专注于.NetCore的技术小白
  • 相关阅读:
    (转)extern用法详解
    (转)extern用法,全局变量与头文件
    关于将数字转换成中文表达程序
    不用对战平台玩魔兽
    结构体内存对齐问题(转)
    今天碰到的很奇怪的问题
    99乘法表
    自我检讨
    收支簿
    掠夺论
  • 原文地址:https://www.cnblogs.com/shanfeng1000/p/14482297.html
Copyright © 2020-2023  润新知