【.NET Core框架】选项模式(Options)
简介
.net core中有两种读取配置的方式:
- 通过注入IConfiguration读取配置
- 通过强类型的Options,通过注入来获取到配置对象。
Options类的命名规则:{Object}Options
建议使用强类型的Options,这样在你想获取某个配置时,只需要注入对应的Options,而不是获取整个配置
选项的优点
- 强类型配置
- 提供多种读取方式
- 支持热加载
- 支持设置默认值
- 支持验证配置有效性
- 支持更改通知
- 支持命名选项
涉及的nuget包
Microsoft.Extensions.Options: 选项模型本身就这一个包(构建在依赖注入容器之上);
Microsoft.Extensions.Options.ConfigurationExtensions:用于和“配置模块”集成,集成后可以从“配置模块”提取数据;
Options绑定
public void ConfigureServices(IServiceCollection services)
{
// 方式 1:
var bookOptions1 = new BookOptions();
Configuration.GetSection(BookOptions.Book).Bind(bookOptions1);
// 方式 2:
var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
}
Options注册
Options框架为我们提供了一些的IServiceCollection
的扩展方法,方便我们的使用。
Configure方法
通过IConfiguration进行配置:
// 不指定名称
services.Configure<MyOptions>(Configuration.GetSection("Sign"));
// 指定具体名称
services.Configure<MyOptions>("my", Configuration.GetSection("Sign"));
// 配置所有实例
services.ConfigureAll<MyOptions>(Configuration.GetSection("Sign"));
通过Action配置:
services.Configure<MyOptions>(o => o.DefaultValue = true);
services.Configure<MyOptions>("my", o => o.DefaultValue = true);
services.ConfigureAll<MyOptions>(o => o.DefaultValue = true);
命名选项
上面我们提到了命名选项,命名选项常用于多个配置节点绑定同一属性的情况,举例:
{
"DateTime": {
"Beijing": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":12
},
"Tokyo": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":13
},
}
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.Configure<DateTimeOptions>("Beijing", Configuration.GetSection($"DateTime:Beijing"));
services.Configure<DateTimeOptions>("Tokyo", Configuration.GetSection($"DateTime:Tokyo"));
}
通过IConfigureOptions配置:
更灵活的注册方式,以上两种方式也是通过IConfigureOptions方式注册的
public class ConfigureEmailOptions : IConfigureOptions<EmailOptions>
{
private readonly IConfiguration _configuration;
public ConfigureEmailOptions(IConfiguration configuration)
{
this._configuration = configuration;
}
public void Configure(EmailOptions options)
{
options.Url = _configuration["Url"];
}
}
services.AddSingleton<IConfigureOptions<EmailOptions>, ConfigureEmailOptions>();
使用 DI 服务配置选项
在某些场景下,选项的配置需要依赖DI中的服务,这时可以借助OptionsBuilder的Configure方法(注意这个Configure不是上面提到的IServiceCollection的扩展方法Configure,这是两个不同的方法),该方法支持最多5个服务来配置选项:
services.AddOptions<BookOptions>()
.Configure<Service1, Service2, Service3, Service4, Service5>((o, s, s2, s3, s4, s5) =>
{
o.Authors = DoSomethingWith(s, s2, s3, s4, s5);
});
第二种方式:
builder.Services.AddTransient<Order>(p => new Order() { ID = 10000 });
builder.Services.Configure<OrderOptions>(options =>options.Site="cnblog.com");
builder.Services.AddTransient<IConfigureOptions<OrderOptions>>(sp =>
new ConfigureNamedOptions<OrderOptions, Order>(string.Empty, sp.GetRequiredService<Order>(), (options,order) => {
options.OrderID = order.ID;
}));
Options读取
通过Options接口,我们可以读取依赖注入容器中的Options。常用的有三个接口:
- IOptions
- IOptionsSnapshot
- IOptionsMonitor
IOptions
- 该接口对象实例生命周期为 Singleton,因此能够将该接口注入到任何生命周期的服务中
- 当该接口被实例化后,其中的选项值将永远保持不变
- 不支持命名选项(Named Options)
IOptionsSnapshot
- 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。
- 在作用域中,创建IOptionsSnapshot
对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。 - 支持命名选项
IOptionsMonitor
该接口除了可以查看TOptions的值,还可以监控TOptions配置的更改。
该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中
每次读取选项值时,都是从配置中读取最新选项值(CurrentValue),并当配置发生更改时,进行通知(OnChange)
支持命名选项
Options后期配置
Configure之后执行,可以为Options设置默认值,也可以做数据校验
介绍两个方法,分别是PostConfigure和PostConfigureAll,他们用来对选项进行后期配置。
- 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行
- 与Configure和ConfigureAll类似,PostConfigure仅用于对指定名称的选项进行后期配置(默认名称为string.Empty),PostConfigureAll则用于对所有选项实例进行后期配置
- 每当选项更改时,均会触发相应的方法
public void ConfigureServices(IServiceCollection services)
{
services.PostConfigure<DateTimeOptions>(options =>
{
Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置");
});
services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>
{
Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置");
});
services.PostConfigureAll<DateTimeOptions>(options =>
{
Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置");
});
}
PostConfigure案例:
public static IServiceCollection AddAliyunService(this IServiceCollection serviceCollection, Action<AliyunServiceOption> configAction)
{
if (configAction != null)
{
serviceCollection.Configure(configAction);
}
serviceCollection.PostConfigure<AliyunServiceOption>(options =>
{
if (string.IsNullOrWhiteSpace(options.AccessKeyId))
{
throw new ArgumentNullException(nameof(options.AccessKeyId), $"{nameof(AliyunServiceOption)} {nameof(options.AccessKeyId)} can not be null or empty");
}
if (string.IsNullOrWhiteSpace(options.AccessKeySecret))
{
throw new ArgumentNullException(nameof(options.AccessKeySecret), $"{nameof(AliyunServiceOption)} {nameof(options.AccessKeySecret)} can not be null or empty");
}
});
serviceCollection.TryAddSingleton<IAliyunService, AliyunService>();
return serviceCollection;
}
Options验证
配置毕竟是我们手动进行文本输入的,难免会出现错误,这种情况下,就需要使用程序来帮助进行校验了。
Install-Package Microsoft.Extensions.Options.DataAnnotations
我们先升级一下BookOptions,增加一些数据校验:
public class BookOptions
{
public const string Book = "Book";
[Range(1,1000,
ErrorMessage = "必须 {1} <= {0} <= {2}")]
public int Id { get; set; }
[StringLength(10, MinimumLength = 1,
ErrorMessage = "必须 {2} <= {0} Length <= {1}")]
public string Name { get; set; }
public string Author { get; set; }
}
然后我们在添加到DI容器时,增加数据注解验证:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<BookOptions>()
.Bind(Configuration.GetSection(BookOptions.Book))
.ValidateDataAnnotations();
.Validate(options =>
{
// 校验通过 return true
// 校验失败 return false
if (options.Author.Contains("A"))
{
return false;
}
return true;
});
}
ValidateDataAnnotations会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用Validate进行较为复杂的校验,如果过于复杂,则就要用到IValidateOptions了(实质上,Validate方法内部也是通过注入一个IValidateOptions实例来实现选项验证的)。
IValidateOptions
通过实现IValidateOptions
public class BookValidation : IValidateOptions<BookOptions>
{
public ValidateOptionsResult Validate(string name, BookOptions options)
{
var failures = new List<string>();
if(!(options.Id >= 1 && options.Id <= 1000))
{
failures.Add($"必须 1 <= {nameof(options.Id)} <= {1000}");
}
if(!(options.Name.Length >= 1 && options.Name.Length <= 10))
{
failures.Add($"必须 1 <= {nameof(options.Name)} <= 10");
}
if (failures.Any())
{
return ValidateOptionsResult.Fail(failures);
}
return ValidateOptionsResult.Success;
}
}
然后我们将其注入到DI容器 Singleton,这里使用了TryAddEnumerable扩展方法添加该服务,是因为我们可以注入多个针对同一Options的IValidateOptions,这些IValidateOptions实例都会被执行:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<BookOptions>, BookValidation>());
}
Configure方法源码解析
使用Action的Configure方法
Configure方法是为IConfigureOptions<>
注册了一个单例ConfigureNamedOptions<>
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
where TOptions : class
{
services.AddOptions();
services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
return services;
}
而不指定name
的Configure
和ConfigureAll
方法,都只是一种简写形式,使用默认的DefaultName
和null
public static class Options
{
public static readonly string DefaultName = string.Empty;
}
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(Options.Options.DefaultName, configureOptions);
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(name: null, configureOptions: configureOptions);
使用IConfiguration的Configure方法
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config)
where TOptions : class
{
services.AddOptions();
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config));
}
注册的实例变成了NamedConfigureFromConfigurationOptions
,NamedConfigureFromConfigurationOptions<TOptions>
继承自ConfigureNamedOptions<TOptions>
,所以其本质依然是ConfigureNamedOptions
,只不过Action的方法体变成了ConfigurationBinder.Bind()
:
public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
where TOptions : class
{
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
: base(name, options => ConfigurationBinder.Bind(config, options))
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
}
}
ConfigureNamedOptions
ConfigureNamedOptions
实现了IConfigureNamedOptions
,而IConfigureNamedOptions
则是对IConfigureOptions
的一个扩展,添加了Name参数,这样,我们便可以为同一个Options
类型注册多个独立的实例,在某些场景下则是非常有用的。
IConfigureOptions
接口:
public interface IConfigureOptions<in TOptions> where TOptions : class
{
void Configure(TOptions options);
}
IConfigureNamedOptions
接口:
public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
void Configure(string name, TOptions options);
}
ConfigureNamedOptions
的源码:
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions>, IConfigureOptions<TOptions> where TOptions : class
{
public ConfigureNamedOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
}
public string Name { get; }
public Action<TOptions> Action { get; }
public virtual void Configure(string name, TOptions options)
{
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}
ConfigureNamedOptions
本质上就是把我们注册的Action
包装成统一的Configure
方法,以方便后续创建Options实例时,进行初始化。
AddOptions()
方法源码
上面的Configure()
方法中都调用了AddOptions()
方法,它是用来注册Options模式相关的核心对象
public static IServiceCollection AddOptions(this IServiceCollection 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;
}
IOptions
单例,它会一直缓存选项对象
public interface IOptions<out TOptions> where TOptions : class, new()
{
TOptions Value { get; }
}
IOptionsSnapshot
Scope,每个请求都重读配置以获得新的选项对象(支持热更新)
先看IOptionsSnapshot接口的定义,IOptionsSnapshot还继承了IOptions,而且只是多了一个Get方法
public interface IOptionsSnapshot<out TOptions> : IOptions<TOptions> where TOptions : class, new()
{
TOptions Get(string name);
}
再看它们的实现类都是OptionsManager<>
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
区别是生命周期不同,单例的IOptions
意味着,只要您注入之后以后获取的都是同一个实例,而IOptionsSnapshot
呢,作为Scoped级别,每再一个新的Scoped中获取它一次,它就会请求一个新的实例。
所以来举个例子,在AspNet Core中咱们某个选项的值是根据一个文件的某个值来的。刚开始文本的值是“A”,咱们在运行AspNet Core之后我们获取IOptions<MyOptions>
和IOptionsSnapshot<MyOptions>
,此时得到的MyOptions的该属性的值都是"A"。但是假如我们更改了文本的值,改为“B”。如果在发起一个http请求去获取MyOptions的结果,此时IOptions<MyOptions>
依旧是“A”,而IOptionsSnapshot<MyOptions>
则更改为了B。
OptionsManager
OptionsManager
对options的创建和获取进行管理,OptionsManager
使用IOptionsFactory
来创建options对象,并使用内部属性OptionsCache<TOptions> _cache
进行缓存。
源码:
public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // cache
public OptionsManager(IOptionsFactory<TOptions> factory)
{
_factory = factory;
}
public TOptions Value => Get(Options.DefaultName);
public virtual TOptions Get(string name) => _cache.GetOrAdd(name, () => _factory.Create(name ?? Options.DefaultName));
}
OptionsFactory
public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new()
{
private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
{
_setups = setups;
_postConfigures = postConfigures;
}
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);
}
return options;
}
}
OptionsCache
OptionsCache 则是对字典的一个简单封装
源码:
public class OptionsCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
{
private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);
public void Clear() => _cache.Clear();
public virtual TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, new Lazy<TOptions>(createOptions)).Value;
}
public virtual bool TryAdd(string name, TOptions options)
{
name = name ?? Options.DefaultName;
return _cache.TryAdd(name, new Lazy<TOptions>(() => options));
}
public virtual bool TryRemove(string name)
{
name = name ?? Options.DefaultName;
return _cache.TryRemove(name, out var ignored);
}
}
IOptionsMonitor
一直缓存选项对象,但当配置源发生更改时自动更新我的选项对象时使用(支持热更新,并缓存,推荐)
IOptionsMonitor<>
接口的实现类是OptionsMonitor<>
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
IOptionsMonitor
,更强大的Options,它的用法和IOptionsSnapshot
没有区别,不同的时,它多了一个配置文件发生改变之后事件处理
public interface IOptionsMonitor<out TOptions>
{
TOptions CurrentValue { get; }
TOptions Get(string name);
IDisposable OnChange(Action<TOptions, string> listener);
}
IOptionsMonitor<>
的注入级别虽然是单例,但是因为它具有IChangeToken的实现,所以它能够在选项源改变的时候,“立马对选项做出对应的改变”。而改变依赖于IOptionsChangeTokenSource这个令牌源,目前.net core对很多常用工具都实现了该令牌源,比如Logger,Configuration等。所以当我们某个选项依赖于IConfiguration(appsetting.json)的某一项时,当修改appsetting.json文件,该选项的值就能够立马得到更改。
假如把IOptionsMonitor<MyOptions>
添加到上面IOptions<MyOptions>
和IOptionsSnapshot<MyOptions>
的文件变更案例,如果在一次HTTP请求中,文件变更了两次,那么IOptionsSnapshot<MyOptions>
不会在第二次更改中同步更改,而IOptionsMonitor<MyOptions>
则可以。
那么什么时候来使用什么样的接口呢?当您的选项只是负责一次性处理的时候,应用启动了就不需要更改,那么考虑使用IOptions<MyOptions>
,如果是对数据源的变更要求很严格,比如开启了一个“BackgroundJob”在后台运行,该job需要一个选项类型,而该类型依赖于配置文件,需要对配置文件更改时即刻做出改变,那么请考虑使用IOptionsMonitor<MyOptions>
。
OptionsMonitor
源码:
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptionsMonitor<TOptions>,
IDisposable
where TOptions : class
{
private readonly IOptionsMonitorCache<TOptions> _cache;
private readonly IOptionsFactory<TOptions> _factory;
private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
private readonly List<IDisposable> _registrations = new List<IDisposable>();
internal event Action<TOptions, string> _onChange;
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
}
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
public TOptions CurrentValue
{
get => Get(Options.DefaultName);
}
/// <summary>
/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
/// </summary>
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public IDisposable OnChange(Action<TOptions, string> listener)
{
var disposable = new ChangeTrackerDisposable(this, listener);
_onChange += disposable.OnChange;
return disposable;
}
public void Dispose()
{
// Remove all subscriptions to the change tokens
foreach (IDisposable registration in _registrations)
{
registration.Dispose();
}
_registrations.Clear();
}
internal class ChangeTrackerDisposable : IDisposable
{
private readonly Action<TOptions, string> _listener;
private readonly OptionsMonitor<TOptions> _monitor;
public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
{
_listener = listener;
_monitor = monitor;
}
public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);
public void Dispose() => _monitor._onChange -= OnChange;
}
}
ConfigurationChangeTokenSource
ConfigurationChangeTokenSource
是接口IOptionsChangeTokenSource
的实现类
源码:
public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
{
private IConfiguration _config;
public ConfigurationChangeTokenSource(IConfiguration config) : this(Options.DefaultName, config) { }
public ConfigurationChangeTokenSource(string name, IConfiguration config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
_config = config;
}
public string Name { get; }
public IChangeToken GetChangeToken()
{
return _config.GetReloadToken();
}
}
参考:
https://www.cnblogs.com/artech/p/inside-asp-net-core-3.html
https://www.cnblogs.com/RainingNight/tag/Options/
https://www.cnblogs.com/uoyo/p/12583149.html
https://www.cnblogs.com/xiaoxiaotank/p/15391905.html