.NET Core 选项系统的主要实现在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 两个 Nuget 包。对于一个框架的源码进行解读,我们可以从我们常用的框架中的类或方法入手,这些类或方法就是我们解读的入口。
从上面对选项系统的介绍中,大家也可以看出,日常对选项系统的使用涉及到的主要有 Configure 方法,有 IOptions
Configure
首先看选项注册,也就是 Configure 方法,注册相关的方法都是扩展方法,上面也讲到 Configure 方法有多个扩展来源,其中最常用的是 OptionsConfigurationServiceCollectionExtensions 中的 Configure 方法,该方法用于从配置信息中读取配置并绑定为选项,如下,这里将相应的方法单独摘出来了。
点击查看代码 OptionsConfigurationServiceCollectionExtensions.Configure
public static class OptionsConfigurationServiceCollectionExtensions
{
///
/// Registers a configuration instance which TOptions will bind against.
///
/// The type of options being configured.
/// The to add the services to.
/// The configuration being bound.
/// The so that additional calls can be chained.
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
=> services.Configure(Options.Options.DefaultName, config);
///
/// Registers a configuration instance which TOptions will bind against.
///
/// The type of options being configured.
/// The to add the services to.
/// The name of the options instance.
/// The configuration being bound.
/// The so that additional calls can be chained.
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
=> services.Configure(name, config, _ => { });
///
/// Registers a configuration instance which TOptions will bind against.
///
/// The type of options being configured.
/// The to add the services to.
/// The configuration being bound.
/// Used to configure the .
/// The so that additional calls can be chained.
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config, Action configureBinder )
where TOptions : class
=> services.Configure(Options.Options.DefaultName, config, configureBinder);
///
/// Registers a configuration instance which TOptions will bind against.
///
/// The type of options being configured.
/// The to add the services to.
/// The name of the options instance.
/// The configuration being bound.
/// Used to configure the .
/// The so that additional calls can be chained.
[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config, Action configureBinder )
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
services.AddOptions();
services.AddSingleton>(new ConfigurationChangeTokenSource(name, config));
return services.AddSingleton>(new NamedConfigureFromConfigurationOptions(name, config, configureBinder));
}
}
其中 IOptionsChangeTokenSource
另外还有 OptionsServiceCollectionExtensions 中的 Configure 方法,用于直接通过委托对选项类进行配置。
点击查看代码 OptionsServiceCollectionExtensions.Configure
public static class OptionsServiceCollectionExtensions
{
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action configureOptions ) where TOptions : class
=> services.Configure(Options.Options.DefaultName, configureOptions);
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action configureOptions )
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.AddOptions();
services.AddSingleton>(new ConfigureNamedOptions(name, configureOptions));
return services;
}
}
可以看出,其实选项系统中的选项都是命名模式的,默认名称为 Options.Options.DefaultName,实际就是 string.Empty。当我们调用 Configure 方法对选项进行配置的时候,实际上时调用了 AddOptions 方法,并且往容器中添加了一个单例的实现了 IConfigureOptions
IConfigureOptions、IConfigureNamedOptions、IPostConfigureOptions
其中 IConfigureOptions
点击查看代码 ConfigureOptions
public class ConfigureOptions : IConfigureOptions where TOptions : class
{
///
/// Constructor.
///
/// The action to register.
public ConfigureOptions(Action action)
{
Action = action;
}
///
/// The configuration action.
///
public Action Action { get; }
///
/// Invokes the registered configure .
///
/// The options instance to configure.
public virtual void Configure(TOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
Action?.Invoke(options);
}
}
IConfigureNamedOptions
点击查看代码 ConfigureNamedOptions
```csharp
public class ConfigureNamedOptions/// Constructor. ///
/// The name of the options. /// The action to register. public ConfigureNamedOptions(string name, Action///
/// The options name.
///
public string Name { get; }
///
/// The configuration action.
///
public Action Action { get; }
///
/// Invokes the registered configure if the matches.
///
/// The name of the options instance being configured.
/// The options instance to configure.
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);
}
}
///
/// Invoked to configure a instance with the .
///
/// The options instance to configure.
public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}
点击查看代码 NamedConfigureFromConfigurationOptions
```csharp
public class NamedConfigureFromConfigurationOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions> : ConfigureNamedOptions<TOptions>
where TOptions : class
{
/// /// Constructor that takes the
/// Constructor that takes the
其他的 IPostConfigureOptions 接口也是一样套路,当我们通过相应的方法传入委托对选项类进行配置的时候,会向容器中注入一个单例服务,将配置行为保存起来。
接着往下看 AddOptions 方法,AddOptions 方法有两个重载:
点击查看代码 AddOptions
```csharp public static class OptionsServiceCollectionExtensions { public static IServiceCollection AddOptions(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
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;
}
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddOptions();
return new OptionsBuilder(services, name);
}
}
点击查看代码 OptionsBuilder
```csharp
public class OptionsBuilder<TOptions> where TOptions : class
{
private const string DefaultValidationFailureMessage = "A validation error has occurred.";
public string Name { get; }
public IServiceCollection Services { get; }
public OptionsBuilder(IServiceCollection services, string name)
{
Services = services;
Name = name ?? Options.DefaultName;
}
public virtual OptionsBuilder<TOptions> Configure(ActionIValidateOptions
我们除了可以对选项进行配置绑定之外,还可以对选项进行验证。验证规则是通过上面的第二个 AddOptions 方法返回的 OptionsBuilder
验证规则配置有三种方式,最后其实都是通过 IValidateOptions
ValidateOptions
点击查看代码 ValidateOptions
public class ValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
{
///
/// Constructor.
///
/// Options name.
/// Validation function.
/// Validation failure message.
public ValidateOptions(string name, Funcbool > validation, string failureMessage)
{
Name = name;
Validation = validation;
FailureMessage = failureMessage;
}
///
/// The options name.
///
public string Name { get; }
///
/// The validation function.
///
public Funcbool> Validation { get; }
///
/// The error to return when validation fails.
///
public string FailureMessage { get; }
///
/// Validates a specific named options instance (or all when is null).
///
/// The name of the options instance being validated.
/// The options instance.
/// The result.
public ValidateOptionsResult Validate(string name, TOptions options)
{
// null name is used to configure all named options
if (Name == null || name == Name)
{
if ((Validation?.Invoke(options)).Value)
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(FailureMessage);
}
// ignored if not validating this instance
return ValidateOptionsResult.Skip;
}
}
我们可以通过重载方法传入相应的验证失败提醒文本。
Options、UnnamedOptionsManager
接下来看选项使用相关的内容,其中 IOptions
点击查看代码 UnnamedOptionsManager
internal sealed class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptions<TOptions>
where TOptions : class
{
private readonly IOptionsFactory _factory;
private volatile object _syncObj;
private volatile TOptions _value;
public UnnamedOptionsManager(IOptionsFactory factory ) => _factory = factory;
public TOptions Value
{
get
{
if (_value is TOptions value)
{
return value;
}
lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
{
return _value ??= _factory.Create(Options.DefaultName);
}
}
}
}
IOptions
IOptionsSnapshot、OptionsManager
IOptionsSnapshot
点击查看代码 OptionsManager
public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptions<TOptions>,
IOptionsSnapshot<TOptions>
where TOptions : class
{
private readonly IOptionsFactory _factory;
private readonly OptionsCache _cache = new OptionsCache(); // Note: this is a private cache
///
/// Initializes a new instance with the specified options configurations.
///
/// The factory to use to create options.
public OptionsManager(IOptionsFactory factory )
{
_factory = factory;
}
///
/// The default configured instance, equivalent to Get(Options.DefaultName).
///
public TOptions Value => Get(Options.DefaultName);
///
/// Returns a configured instance with the given .
///
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
if (!_cache.TryGetValue(name, out TOptions options))
{
// Store the options in our instance cache. Avoid closure on fast path by storing state into scoped locals.
IOptionsFactory localFactory = _factory;
string localName = name;
options = _cache.GetOrAdd(name, () => localFactory.Create(localName));
}
return options;
}
}
IOptionsMonitor、OptionsMonitor
IOptionsMonitor
点击查看代码 OptionsMonitor
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptionsMonitor<TOptions>,
IDisposable
where TOptions : class
{
private readonly IOptionsMonitorCache _cache;
private readonly IOptionsFactory _factory;
private readonly List _registrations = new List();
internal event Actionstring> _onChange;
///
/// Constructor.
///
/// The factory to use to create options.
/// The sources used to listen for changes to the options instance.
/// The cache used to store options.
public OptionsMonitor(IOptionsFactory factory, IEnumerable> sources, IOptionsMonitorCache cache )
{
_factory = factory;
_cache = cache;
void RegisterSource(IOptionsChangeTokenSource source )
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
// The default DI container uses arrays under the covers. Take advantage of this knowledge
// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
if (sources is IOptionsChangeTokenSource[] sourcesArray)
{
foreach (IOptionsChangeTokenSource source in sourcesArray)
{
RegisterSource(source);
}
}
else
{
foreach (IOptionsChangeTokenSource source in sources)
{
RegisterSource(source);
}
}
}
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
///
/// The present value of the options.
///
public TOptions CurrentValue
{
get => Get(Options.DefaultName);
}
///
/// Returns a configured instance with the given .
///
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
///
/// Registers a listener to be called whenever changes.
///
/// The action to be invoked when has changed.
/// An which should be disposed to stop listening for changes.
public IDisposable OnChange(Actionstring > listener)
{
var disposable = new ChangeTrackerDisposable(this, listener);
_onChange += disposable.OnChange;
return disposable;
}
///
/// Removes all change registration subscriptions.
///
public void Dispose()
{
// Remove all subscriptions to the change tokens
foreach (IDisposable registration in _registrations)
{
registration.Dispose();
}
_registrations.Clear();
}
internal sealed class ChangeTrackerDisposable : IDisposable
{
private readonly Actionstring> _listener;
private readonly OptionsMonitor _monitor;
public ChangeTrackerDisposable(OptionsMonitor monitor, Actionstring > listener)
{
_listener = listener;
_monitor = monitor;
}
public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);
public void Dispose() => _monitor._onChange -= OnChange;
}
}
OnChange 方法中传入的委托本来可以可以直接追加到事件中的,这里将其再包装多一层,是为了 OptionsMonitor 对象销毁的时候能够将相应的事件释放,如果不包装多一层的话,委托只在方法作用域中,对象释放的时候是获取不到的。
IOptionsMonitorCache、OptionsCache
OptionsCache 是 IOptionsMonitorCache 接口的的实现类,从上面可以看到 OptionsMonitor
OptionsCache 的具体实现比较简单,主要就是通过 ConcurrentDictionary
IOptionsFactory、OptionsFactory
OptionsFactory
点击查看代码 OptionsFactory
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptionsFactory<TOptions>
where TOptions : class
{
private readonly IConfigureOptions[] _setups;
private readonly IPostConfigureOptions[] _postConfigures;
private readonly IValidateOptions[] _validations;
///
/// Initializes a new instance with the specified options configurations.
///
/// The configuration actions to run.
/// The initialization actions to run.
public OptionsFactory(IEnumerable> setups, IEnumerable> postConfigures ) : this(setups, postConfigures, validations: Array.Empty>( ))
{ }
///
/// Initializes a new instance with the specified options configurations.
///
/// The configuration actions to run.
/// The initialization actions to run.
/// The validations to run.
public OptionsFactory(IEnumerable> setups, IEnumerable> postConfigures, IEnumerable> validations )
{
// The default DI container uses arrays under the covers. Take advantage of this knowledge
// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
// When it isn't already an array, convert it to one, but don't use System.Linq to avoid pulling Linq in to
// small trimmed applications.
_setups = setups as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(setups).ToArray();
_postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigures).ToArray();
_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
}
///
/// Returns a configured instance with the given .
///
public TOptions Create(string name)
{
TOptions options = CreateInstance(name);
foreach (IConfigureOptions setup in _setups)
{
if (setup is IConfigureNamedOptions namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
foreach (IPostConfigureOptions post in _postConfigures)
{
post.PostConfigure(name, options);
}
if (_validations != null)
{
var failures = new List<string>();
foreach (IValidateOptions validate in _validations)
{
ValidateOptionsResult result = validate.Validate(name, options);
if (result is not null && result.Failed)
{
failures.AddRange(result.Failures);
}
}
if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
///
/// Creates a new instance of options type
///
protected virtual TOptions CreateInstance(string name)
{
return Activator.CreateInstance();
}
}
以上就是 .NET Core 下的选项系统,由于选项系统的源码不多,这里也就将大部分类都拿出来讲了一下,相当于把这个框架的流程思路都讲了一遍,不知不觉写得字数又很多了,希望有童鞋能够耐心地看到这里。
参考文章:
ASP.NET Core 中的选项模式 | Microsoft Learn
选项模式 - .NET | Microsoft Learn
面向 .NET 库创建者的选项模式指南 - .NET | Microsoft Learn
理解ASP.NET Core - 选项(Options)
ASP.NET Core 系列:
目录:ASP.NET Core 系列总结
上一篇:ASP.NET Core - 选项系统之选项验证
下一篇:ASP.NET Core - 缓存之内存缓存(上)