每日短讯:ASP.NET Core - 选项系统之源码介绍
.NET Core 选项系统的主要实现在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 两个 Nuget 包。对于一个框架的源码进行解读,我们可以从我们常用的框架中的类或方法入手,这些类或方法就是我们解读的入口。
从上面对选项系统的介绍中,大家也可以看出,日常对选项系统的使用涉及到的主要有 Configure 方法,有 IOptions
首先看选项注册,也就是 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(this IServiceCollection services, Action configureOptions) where TOptions : class=> services.Configure(Options.Options.DefaultName, configureOptions);public static IServiceCollection Configure(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
点击查看代码 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
```csharppublic 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
```csharppublic class NamedConfigureFromConfigurationOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions> : ConfigureNamedOptions/// Constructor that takes the instance to bind against.///
/// The name of the options instance./// The /// Constructor that takes the instance to bind against.///
/// The name of the options instance./// The 其他的 IPostConfigureOptions 接口也是一样套路,当我们通过相应的方法传入委托对选项类进行配置的时候,会向容器中注入一个单例服务,将配置行为保存起来。
接着往下看 AddOptions 方法,AddOptions 方法有两个重载:
点击查看代码 AddOptions
```csharppublic 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 AddOptions(this IServiceCollection services, string name)where TOptions : class{if (services == null){throw new ArgumentNullException(nameof(services));}services.AddOptions();return new OptionsBuilder(services, name);} }
点击查看代码 OptionsBuilder
```csharppublic class OptionsBuilder我们除了可以对选项进行配置绑定之外,还可以对选项进行验证。验证规则是通过上面的第二个 AddOptions 方法返回的 OptionsBuilder
验证规则配置有三种方式,最后其实都是通过 IValidateOptions
ValidateOptions
点击查看代码 ValidateOptions
public class ValidateOptions : IValidateOptions where TOptions : class{/// /// Constructor.///
/// Options name./// Validation function./// Validation failure message.public ValidateOptions(string name, Func validation, string failureMessage){Name = name;Validation = validation;FailureMessage = failureMessage;}/// /// The options name.///
public string Name { get; }/// /// The validation function.///
public Func 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 optionsif (Name == null || name == Name){if ((Validation?.Invoke(options)).Value){return ValidateOptionsResult.Success;}return ValidateOptionsResult.Fail(FailureMessage);}// ignored if not validating this instancereturn ValidateOptionsResult.Skip;}} 我们可以通过重载方法传入相应的验证失败提醒文本。
Options接下来看选项使用相关的内容,其中 IOptions
点击查看代码 UnnamedOptionsManager
internal sealed class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptionswhere 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
public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptions,IOptionsSnapshotwhere 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
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptionsMonitor,IDisposablewhere TOptions : class{private readonly IOptionsMonitorCache _cache;private readonly IOptionsFactory _factory;private readonly List _registrations = new List();internal event Action _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(Action 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 tokensforeach (IDisposable registration in _registrations){registration.Dispose();}_registrations.Clear();}internal sealed class ChangeTrackerDisposable : IDisposable{private readonly Action _listener;private readonly OptionsMonitor _monitor;public ChangeTrackerDisposable(OptionsMonitor monitor, Action 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、OptionsCacheOptionsCache 是 IOptionsMonitorCache 接口的的实现类,从上面可以看到 OptionsMonitor
OptionsCache 的具体实现比较简单,主要就是通过 ConcurrentDictionary
OptionsFactory
点击查看代码 OptionsFactory
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptionsFactorywhere 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[] ?? new List>(setups).ToArray();_postConfigures = postConfigures as IPostConfigureOptions[] ?? new List>(postConfigures).ToArray();_validations = validations as IValidateOptions[] ?? new List>(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();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 - 选项系统之选项验证
标签: