.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开发的基础,上面的例子也比较清楚,应该都能理解吧。
.net core的配置介绍(三):Options的更多相关文章
- .net core的配置介绍(一):IConfiguration
说到配置,绝大部分系统都会有配置,不需要配置的系统是非常少的,想想以前做.net 开发时,我们常常将配置放到web.config中,然后使用ConfigurationManager去读取. 初次接触到 ...
- .net core的配置介绍(二):自定义配置(Zookeeper,数据库,Redis)
上一篇介绍了.net core的配置原理已经系统提供的一些常用的配置,但有时我们的配置是存放在Zookeeper,DB,Redis中的,这就需要我们自己去实现集成了. 这里再介绍几个我们用的多的配置集 ...
- .NET Core采用的全新配置系统[3]: “Options模式”下的配置是如何绑定为Options对象
配置的原子结构就是单纯的键值对,并且键和值都是字符串,但是在真正的项目开发中我们一般不会单纯地以键值对的形式来使用配置.值得推荐的做法就是采用<.NET Core采用的全新配置系统[1]: 读取 ...
- ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍
在上一章中,我们介绍了 ASP.NET Core 的启动过程,主要是对 WebHost 源码的探索.而本文则是对上文的一个补充,更加偏向于实战,详细的介绍一下我们在实际开发中需要对 Hosting 做 ...
- .NET Core IdentityServer4实战 第三章-使用EntityFramework Core进行持久化配置
内容:本文带大家使用IdentityServer4进行使用使用EntityFramework Core进行配置和操作数据 作者:zara(张子浩) 欢迎分享,但需在文章鲜明处留下原文地址. 前两章内容 ...
- 【转】Spring学习---Bean配置的三种方式(XML、注解、Java类)介绍与对比
[原文]https://www.toutiao.com/i6594205115605844493/ Spring学习Bean配置的三种方式(XML.注解.Java类)介绍与对比 本文将详细介绍Spri ...
- MyCat教程三:安装及配置介绍
一.安装MyCat 1.安装准备环境 1.1 安装JDK 因为MyCat是java开发的,所以需要java虚拟机环境,在Linux节点中安装JDK是必须的. 1.2 放开相关端口 在主从节点上 ...
- ASP.NET Core的配置(4):多样性的配置来源[上篇]
较之传统通过App.config和Web.config这两个XML文件承载的配置系统,ASP.NET Core采用的这个全新的配置模型的最大一个优势就是针对多种不同配置源的支持.我们可以将内存变量.命 ...
- ASP.NET Core的配置(3): 将配置绑定为对象[上篇]
出于编程上的便利,我们通常不会直接利用ConfigurationBuilder创建的Configuration对象读取某个单一配置项的值,而是倾向于将一组相关的配置绑定为一个对象,我们将后者称为Opt ...
随机推荐
- 【Linux】【Basis】Grub
GRUB(Boot Loader): 1. grub: GRand Unified Bootloader grub 0.x: grub legacy grub 1.x: grub2 2. gr ...
- springboot 设置项目路劲后不能访问首页
环境背景 学习版本 : springboot2.31 controller 代码 @controller public class Iindex{ @RequestMapping("/&q ...
- Maven的聚合工程(多模块工程)
在开发2个以上模块的时候,每个模块都是一个 Maven Project.比如搜索平台,学习平台,考试平台.开发的时候可以自己管自己独立编译,测试,运行.但如果想要将他们整合起来,我们就需要一个聚合工程 ...
- 【科研工具】流程图软件Visio Pro 2019 详细安装破解教程
[更新区] 安装教程我下周会在bilibili上传视频,这周事情太多暂时先不弄. [注意] 安装Visio需要和自己的Word版本一样,这里因为我的Word是学校的正版2019(所以学校为什么正版没买 ...
- ANTLR 相关术语
下面介绍很多重要的与语言识别相关的术语. 语言(Language) A language is a set of valid sentences 一门语言是一个有效语句的集合. Sentences a ...
- Mongodb安全防护
1.Mongodb未授权访问 描述 MongoDB 是一个基于分布式文件存储的数据库.默认情况下启动服务存在未授权访问风险,用户可以远程访问数据库,无需认证连接数据库并对数据库进行任意操作,存在严重的 ...
- 如何利用火焰图定位 Java 的 CPU 性能问题
常见 CPU 性能问题 你所负责的服务(下称:服务)是否遇到过以下现象: 休息的时候,手机突然收到大量告警短信,提示服务的 99.9 line 从 20ms 飙升至 10s: 正在敲代码实现业务功能 ...
- 多个工作簿拆分(Excel代码集团)
一个文件夹里有N个工作簿,每个工作簿中包括N个工作表,将各个工作表拆分成工作簿,命名为每个工作簿里第一个工作表的A列和B列. 工作簿.工作表数量不定,表内内容不限,拆分后保存于当前文件夹下的" ...
- 深入理解java虚拟机(一)
java历史 1996.01.23发布Jdk1.0 1998.12.04发布jdk1.2(里程碑的版本)注意:集合容器Collection和Map都是从1.2开始 1999.04.27HotSpot虚 ...
- 当动态桌面遇上 HTML5
DreamScene2 + HTML5 = 无限可能.时隔一周,DreamScene2 动态桌面经过几个小版本的迭代,修复了一些问题并且功能也得到了增强.欢迎 Star 和 Fork https:// ...