在.Net Core中引入了Options这一使用配置方式,通常来讲我们会把所需要的配置通过IConfiguration对象配置成一个普通的类,并且习惯上我们会把这个类的名字后缀加上Options。所以我们在使用某一个中间件,或者使用第三方类库时,经常会看到配置对应Options的代码,例如关于Cookie的中间件就会配置CookiePolicyOptions这一个对象。
在.Net Core中使用Options主要分为两个步骤:
在配置TOptions的时候,你会发现所有的方法都是泛型的,每一个Options类型都有一套独立的管理系统。入口是Configure方法,它有多个重载,但最终都会调用这个方法
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
当不传递name时,默认使用Microsoft.Extensions.Options.DefaultName,等于string.Empty
namespace Microsoft.Extensions.Options
{
/// <summary>
/// Helper class.
/// </summary>
public static class Options
{
public static readonly string DefaultName = string.Empty;
}
}
有的时候我们会看到在调用Configure时并没有传递Action<TOptions>,而是直接传递了一个IConfiguration,那是因为在内部帮我们转化了一下,最终传递的还是一个Action<TOptions>
options => ConfigurationBinder.Bind(config, options)
另外,我们可以看到ConfigureAll这个方法,这个内部也是调用了Configure方法,只不过把name设置成null,后续在创建TOptions时,会把name为nul的Action<TOptions>应用于所有实例。
最后还有一个PostConfigure方法,它和Configure方法使用方式一模一样,也是在创建TOptions时调用。只不过先后顺序不一样,PostConfigure在Configure之后调用。
现在我们来看实际的用法:
services.Configure<EmailOption>(op => op.Title = "Default Name");
services.Configure<EmailOption>("FromMemory", op => op.Title= "FromMemory");
services.Configure<EmailOption>("FromConfiguration", Configuration.GetSection("Email"));
services.AddOptions<EmailOption>("AddOption").Configure(op => op.Title = "AddOption Title");
services.Configure<EmailOption>(null, op => op.From = "Same With ConfigureAll");
//services.ConfigureAll<EmailOption>(op => op.From = "ConfigureAll");
services.PostConfigure<EmailOption>(null, op => op.Body = "Same With PostConfigureAll");
//services.PostConfigureAll<EmailOption>(op => op.Body = "PostConfigurationAll");
EmailOption是一个很简单的类:
public class EmailOption
{
public string Title { get; set; }
public string Body { get; set; }
public string From { get; set; }
}
在上面所示的用法,多了一个AddOptions的用法。这种方式会创建了一个OptionsBuilder,用来辅助配置TOptions对象,其内部实现是和Configure,PostConfigure方法一样的。
既然我们告诉了容器TOption是如何配置的,那么在使用的时候只需要通过注入的方式获取就行了。总共用三种获取方式:
下面我们来看一下具体的使用方法:
public class HomeController : Controller
{
IOptions<EmailOption> _options;
IOptionsMonitor<EmailOption> _optionsMonitor;
IOptionsSnapshot<EmailOption> _optionsSnapshot;
public HomeController(IOptions<EmailOption> options, IOptionsMonitor<EmailOption> optionsMonitor, IOptionsSnapshot<EmailOption> optionsSnapshot)
{
_options = options;
_optionsMonitor = optionsMonitor;
_optionsSnapshot = optionsSnapshot;
}
public IActionResult Demo()
{
EmailOption defaultEmailOption = _options.Value;
EmailOption defaultEmailOption1 = _optionsMonitor.CurrentValue;//_optionsMonitor.Get(Microsoft.Extensions.Options.Options.DefaultName);
EmailOption fromMemoryEmailOption1 = _optionsMonitor.Get("FromMemory");
EmailOption fromConfigurationEmailOption1 = _optionsMonitor.Get("FromConfiguration");
EmailOption defaultEmailOption2 = _optionsSnapshot.Value;//_optionsSnapshot.Get(Microsoft.Extensions.Options.Options.DefaultName);
EmailOption fromMemoryEmailOption2 = _optionsSnapshot.Get("FromMemory");
EmailOption fromConfigurationEmailOption2 = _optionsSnapshot.Get("FromConfiguration");
return View();
}
}
注意:如果是基于IConfiguration的TOptions需要进行监控,必须此IConfiguration是可监控的。
我们在配置Options的时候,其实会向容器内部注入IConfigureOptions<TOptions>或者IConfigureNamedOptions<TOptions>以及IPostConfigureOptions<TOptions>这几种对象。随后负责创建TOptions的工厂类 IOptionsFactory<TOptions>,也以注入的形式获取这几个对象来创建需要的TOptions。其中IConfigureNamedOptions<TOptions>继承于IConfigureOptions<TOptions>。相关的UML图如下:
IOptionsFactory<TOptions>的实现类是OptionsFactory<TOptions>,Create(string name)的核心代码如下:
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;
}
到这里,我们知道了如何通过提供的配置信息,去产生一个TOptions对象。接下来我们看看 IOptions<TOptions>,IOptionsSnapshot<TOptions>,IOptionsMonitor<TOptions>是如何实现的,以及它们是如何实现配置源的动态更新。
每当我们调用Configure方法的时候,系统都会调用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;
}
其中IOptionsFactory就不必说,它就是用来产生对象的,这是它唯一的用处。而IOptions<>,IOptionsSnapshot<>的实现类都是OptionsManager。OptionsManager在创建时会注入IOptionsFactory,同时内部还有一个OptionsCache根据name保存产生的对象。注意:这里的OptionsCache并不是注入到容器里的那个实例。它的代码如下:
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>(); // Note: this is a private cache
/// <summary>
/// Initializes a new instance with the specified options configurations.
/// </summary>
/// <param name="factory">The factory to use to create options.</param>
public OptionsManager(IOptionsFactory<TOptions> factory)
{
_factory = factory;
}
public TOptions Value
{
get
{
return Get(Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
// Store the options in our instance cache
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
}
而IOptionsMonitor是以单例的形式注入到容器中,并且IOptionsMonitorCache也是单例的形式注入到容器中,这个IOptionsMonitorCache后续会在创建OptionsMonitor的时候注入进去,所以OptionsMonitor的缓存也是全局唯一的。但是我们之前已经说过,这个也是能觉察到配置源更新的,那又是如何实现的呢?那是因为还会注入一个IOptionsChangeTokenSource类型,它会觉察到配置源的改变,一旦发生改变就会告知OptionsMonitor从缓存中移除相应的对象。关于移除缓存的核心代码如下:
public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : class, new()
{
private readonly IOptionsMonitorCache<TOptions> _cache;
private readonly IOptionsFactory<TOptions> _factory;
private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;/// <summary>
/// Constructor.
/// </summary>
/// <param name="factory">The factory to use to create options.</param>
/// <param name="sources">The sources used to listen for changes to the options instance.</param>
/// <param name="cache">The cache used to store options.</param>
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
foreach (var source in _sources)
{
ChangeToken.OnChange<string>(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
}
}
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
...
}
}
IOptionsChangeTokenSource需要在配置Options的时候进行配置,如果我们配置的时候调用的IConfiguration的重载,那么他会自动注入一个ConfigurationChangeTokenSource,核心代码如下:
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));
}
既然有如此多的获取方式,那应该如何选择?