.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开发的基础,上面的例子也比较清楚,应该都能理解吧。

 

posted @ 2021-03-10 14:38  没有星星的夏季  阅读(2296)  评论(0编辑  收藏  举报