.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開發的基礎,上面的例子也比較清楚,應該都能理解吧。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM