- A+
.NET Core 选项系统的主要实现在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 两个 Nuget 包。对于一个框架的源码进行解读,我们可以从我们常用的框架中的类或方法入手,这些类或方法就是我们解读的入口。
从上面对选项系统的介绍中,大家也可以看出,日常对选项系统的使用涉及到的主要有 Configure 方法,有 IOptions
Configure
首先看选项注册,也就是 Configure 方法,注册相关的方法都是扩展方法,上面也讲到 Configure 方法有多个扩展来源,其中最常用的是 OptionsConfigurationServiceCollectionExtensions 中的 Configure 方法,该方法用于从配置信息中读取配置并绑定为选项,如下,这里将相应的方法单独摘出来了。
点击查看代码 OptionsConfigurationServiceCollectionExtensions.Configure
public static class OptionsConfigurationServiceCollectionExtensions { /// <summary> /// Registers a configuration instance which TOptions will bind against. /// </summary> /// <typeparam name="TOptions">The type of options being configured.</typeparam> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="config">The configuration being bound.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class => services.Configure<TOptions>(Options.Options.DefaultName, config); /// <summary> /// Registers a configuration instance which TOptions will bind against. /// </summary> /// <typeparam name="TOptions">The type of options being configured.</typeparam> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="name">The name of the options instance.</param> /// <param name="config">The configuration being bound.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class => services.Configure<TOptions>(name, config, _ => { }); /// <summary> /// Registers a configuration instance which TOptions will bind against. /// </summary> /// <typeparam name="TOptions">The type of options being configured.</typeparam> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="config">The configuration being bound.</param> /// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder) where TOptions : class => services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder); /// <summary> /// Registers a configuration instance which TOptions will bind against. /// </summary> /// <typeparam name="TOptions">The type of options being configured.</typeparam> /// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> /// <param name="name">The name of the options instance.</param> /// <param name="config">The configuration being bound.</param> /// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param> /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] 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)); } }
其中 IOptionsChangeTokenSource
另外还有 OptionsServiceCollectionExtensions 中的 Configure 方法,用于直接通过委托对选项类进行配置。
点击查看代码 OptionsServiceCollectionExtensions.Configure
public static class OptionsServiceCollectionExtensions { public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (configureOptions == null) { throw new ArgumentNullException(nameof(configureOptions)); } services.AddOptions(); services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions)); return services; } }
可以看出,其实选项系统中的选项都是命名模式的,默认名称为 Options.Options.DefaultName,实际就是 string.Empty。当我们调用 Configure 方法对选项进行配置的时候,实际上时调用了 AddOptions 方法,并且往容器中添加了一个单例的实现了 IConfigureOptions
IConfigureOptions、IConfigureNamedOptions、IPostConfigureOptions
其中 IConfigureOptions
点击查看代码 ConfigureOptions
public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class { /// <summary> /// Constructor. /// </summary> /// <param name="action">The action to register.</param> public ConfigureOptions(Action<TOptions> action) { Action = action; } /// <summary> /// The configuration action. /// </summary> public Action<TOptions> Action { get; } /// <summary> /// Invokes the registered configure <see cref="Action"/>. /// </summary> /// <param name="options">The options instance to configure.</param> 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
/// <summary> /// The options name. /// </summary> public string Name { get; } /// <summary> /// The configuration action. /// </summary> public Action<TOptions> Action { get; } /// <summary> /// Invokes the registered configure <see cref="Action"/> if the <paramref name="name"/> matches. /// </summary> /// <param name="name">The name of the options instance being configured.</param> /// <param name="options">The options instance to configure.</param> 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); } } /// <summary> /// Invoked to configure a <typeparamref name="TOptions"/> instance with the <see cref="Options.DefaultName"/>. /// </summary> /// <param name="options">The options instance to configure.</param> public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}
</details> 而 NamedConfigureFromConfigurationOptions<TOptions> 类是 IConfigureNamedOptions<TOptions> 的另一个实现,继承了ConfigureNamedOptions<TOptions> 类,重写了一些行为,最终是通过之前讲到的 ConfigurationBuilder的 Bind 方法将配置绑定到选项类而已。 <details> <summary>点击查看代码 NamedConfigureFromConfigurationOptions<TOptions></summary> ```csharp public class NamedConfigureFromConfigurationOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions> : ConfigureNamedOptions<TOptions> where TOptions : class { /// <summary> /// Constructor that takes the <see cref="IConfiguration"/> instance to bind against. /// </summary> /// <param name="name">The name of the options instance.</param> /// <param name="config">The <see cref="IConfiguration"/> instance.</param> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public NamedConfigureFromConfigurationOptions(string name, IConfiguration config) : this(name, config, _ => { }) { } /// <summary> /// Constructor that takes the <see cref="IConfiguration"/> instance to bind against. /// </summary> /// <param name="name">The name of the options instance.</param> /// <param name="config">The <see cref="IConfiguration"/> instance.</param> /// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param> [RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)] public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder) : base(name, options => BindFromOptions(options, config, configureBinder)) { if (config == null) { throw new ArgumentNullException(nameof(config)); } } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The only call to this method is the constructor which is already annotated as RequiresUnreferencedCode.")] private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions> configureBinder) => config.Bind(options, configureBinder); }
其他的 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<TOptions>(services, name); }
}
</details> 这里可以看出两者的返回值不同,而且第二个方法也调用了第一个方法,第一个方法中主要就是向容器中添加我们常用的IOptions<TOptions>、IOptionsSnapshot<TOptions>、IOptionsMonitor<TOptions> 服务接口,这里也可以看到不同服务接口对于的生命周期。除此之外还有工厂服务IOptionsFactory<>和缓存服务IOptionsMonitorCache<>,这两个就是选项体系的关键。每个选项进行配置的时候都会同时注入这些服务,所以每一个选项我们都能使用三个不同接口去解析。 # OptionsBuilder 上面第二个 AddOptions 方法返回 OptionsBuilder<TOptions> 对象。之前讲过 OptionsBuilder<TOptions> 类中也有 Configure 方法,其实不止 Configure 方法,其他的 PostConfigure 方法等也有,它其实就是最终的选项系统配置类,我们所有的选项配置其实都可以通过调用第二个 AddOptions 方法,再通过 OptionsBuilder<TOptions> 对象中的方法来完成配置。其他各个扩展方法的配置方式不过是进行了使用简化而已。 <details> <summary>点击查看代码 OptionsBuilder<TOptions></summary> ```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(Action<TOptions> configureOptions) { Services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(Name, configureOptions)); return this; } public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions) { Services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(Name, configureOptions)); return this; } public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation) => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage) { Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage)); return this; } }
IValidateOptions
我们除了可以对选项进行配置绑定之外,还可以对选项进行验证。验证规则是通过上面的第二个 AddOptions 方法返回的 OptionsBuilder
验证规则配置有三种方式,最后其实都是通过 IValidateOptions
ValidateOptions
点击查看代码 ValidateOptions
public class ValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class { /// <summary> /// Constructor. /// </summary> /// <param name="name">Options name.</param> /// <param name="validation">Validation function.</param> /// <param name="failureMessage">Validation failure message.</param> public ValidateOptions(string name, Func<TOptions, bool> validation, string failureMessage) { Name = name; Validation = validation; FailureMessage = failureMessage; } /// <summary> /// The options name. /// </summary> public string Name { get; } /// <summary> /// The validation function. /// </summary> public Func<TOptions, bool> Validation { get; } /// <summary> /// The error to return when validation fails. /// </summary> public string FailureMessage { get; } /// <summary> /// Validates a specific named options instance (or all when <paramref name="name"/> is null). /// </summary> /// <param name="name">The name of the options instance being validated.</param> /// <param name="options">The options instance.</param> /// <returns>The <see cref="ValidateOptionsResult"/> result.</returns> 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<TOptions> _factory; private volatile object _syncObj; private volatile TOptions _value; public UnnamedOptionsManager(IOptionsFactory<TOptions> 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<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; } /// <summary> /// The default configured <typeparamref name="TOptions"/> instance, equivalent to Get(Options.DefaultName). /// </summary> public TOptions Value => 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; 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<TOptions> 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<TOptions> _cache; private readonly IOptionsFactory<TOptions> _factory; private readonly List<IDisposable> _registrations = new List<IDisposable>(); internal event Action<TOptions, string> _onChange; /// <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; _cache = cache; void RegisterSource(IOptionsChangeTokenSource<TOptions> 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<TOptions>[] sourcesArray) { foreach (IOptionsChangeTokenSource<TOptions> source in sourcesArray) { RegisterSource(source); } } else { foreach (IOptionsChangeTokenSource<TOptions> 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); } } /// <summary> /// The present value of the options. /// </summary> 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)); } /// <summary> /// Registers a listener to be called whenever <typeparamref name="TOptions"/> changes. /// </summary> /// <param name="listener">The action to be invoked when <typeparamref name="TOptions"/> has changed.</param> /// <returns>An <see cref="IDisposable"/> which should be disposed to stop listening for changes.</returns> public IDisposable OnChange(Action<TOptions, string> listener) { var disposable = new ChangeTrackerDisposable(this, listener); _onChange += disposable.OnChange; return disposable; } /// <summary> /// Removes all change registration subscriptions. /// </summary> 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 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; } }
OnChange 方法中传入的委托本来可以可以直接追加到事件中的,这里将其再包装多一层,是为了 OptionsMonitor 对象销毁的时候能够将相应的事件释放,如果不包装多一层的话,委托只在方法作用域中,对象释放的时候是获取不到的。
IOptionsMonitorCache、OptionsCache
OptionsCache 是 IOptionsMonitorCache 接口的的实现类,从上面可以看到 OptionsMonitor
OptionsCache 的具体实现比较简单,主要就是通过 ConcurrentDictionary<string, Lazy
IOptionsFactory、OptionsFactory
OptionsFactory
点击查看代码 OptionsFactory
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsFactory<TOptions> where TOptions : class { private readonly IConfigureOptions<TOptions>[] _setups; private readonly IPostConfigureOptions<TOptions>[] _postConfigures; private readonly IValidateOptions<TOptions>[] _validations; /// <summary> /// Initializes a new instance with the specified options configurations. /// </summary> /// <param name="setups">The configuration actions to run.</param> /// <param name="postConfigures">The initialization actions to run.</param> public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: Array.Empty<IValidateOptions<TOptions>>()) { } /// <summary> /// Initializes a new instance with the specified options configurations. /// </summary> /// <param name="setups">The configuration actions to run.</param> /// <param name="postConfigures">The initialization actions to run.</param> /// <param name="validations">The validations to run.</param> public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> 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(); } /// <summary> /// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>. /// </summary> public TOptions Create(string name) { TOptions options = CreateInstance(name); foreach (IConfigureOptions<TOptions> setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } foreach (IPostConfigureOptions<TOptions> post in _postConfigures) { post.PostConfigure(name, options); } if (_validations != null) { var failures = new List<string>(); foreach (IValidateOptions<TOptions> 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; } /// <summary> /// Creates a new instance of options type /// </summary> protected virtual TOptions CreateInstance(string name) { return Activator.CreateInstance<TOptions>(); } }
以上就是 .NET Core 下的选项系统,由于选项系统的源码不多,这里也就将大部分类都拿出来讲了一下,相当于把这个框架的流程思路都讲了一遍,不知不觉写得字数又很多了,希望有童鞋能够耐心地看到这里。
参考文章:
ASP.NET Core 中的选项模式 | Microsoft Learn
选项模式 - .NET | Microsoft Learn
面向 .NET 库创建者的选项模式指南 - .NET | Microsoft Learn
理解ASP.NET Core - 选项(Options)
ASP.NET Core 系列: