ASP.NET Core - 选项系统之源码介绍
- Configure
- IConfigureOptions、IConfigureNamedOptions、IPostConfigureOptions
- OptionsBuilder
- IValidateOptions
- `Options<TOptions>`、`UnnamedOptionsManager<TOptions>`
- `IOptionsSnapshot<TOptions>`、`OptionsManager<TOptions>`
- `IOptionsMonitor<TOptions>`、`OptionsMonitor<TOptions>`
- IOptionsMonitorCache、OptionsCache
- `IOptionsFactory<TOptions>`、`OptionsFactory<TOptions>`
- 6. 总结
.NET Core 选项系统的主要实现在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 两个 Nuget 包。对于一个框架的源码进行解读,我们可以从我们常用的框架中的类或方法入手,这些类或方法就是我们解读的入口。
从上面对选项系统的介绍中,大家也可以看出,日常对选项系统的使用涉及到的主要有 Configure 方法,有 IOptions<TOptions>
、IOptionsSnapshot<TOptions>
、IOptionMonitor<TOptions>
等接口。
Configure
首先看选项注册,也就是 Configure 方法,注册相关的方法都是扩展方法,之前的章节也讲到 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<TOptions>
接口是用来监听配置变化的服务,这个后面讲。
另外还有 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<TOptions>
接口的实现。
IConfigureOptions、IConfigureNamedOptions、IPostConfigureOptions
其中 IConfigureOptions<TOptions>
是选项配置行为服务接口,ConfigureOptions<TOptions>
是它的默认实现,该类的内容很简单,它的内部主要就是保存了一个委托,用于记录使用者对选项的配置操作。
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<TOptions>
继承了 IConfigureNamedOptions<TOptions>
接口,默认实现是 ConfigureNamedOptions<TOptions>
,作用一样,只不过多了一个方法用于应对命名选项模式。它有多个泛型重载,也是之前的文章ASP.NET Core - 选型系统之选型配置 中讲到的“使用DI服务配置选项”的具体实现。
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{/// <summary>/// Constructor./// </summary>/// <param name="name">The name of the options.</param>/// <param name="action">The action to register.</param>public ConfigureNamedOptions(string name, Action<TOptions> action){Name = name;Action = 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);
}
而 NamedConfigureFromConfigurationOptions<TOptions>
类是 IConfigureNamedOptions<TOptions>
的另一个实现,继承了 ConfigureNamedOptions<TOptions>
类,重写了一些行为,最终是通过之前讲到的 ConfigurationBuilder的 Bind 方法将配置绑定到选项类而已。
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 方法有两个重载:
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);}
}
这里可以看出两者的返回值不同,而且第二个方法也调用了第一个方法,第一个方法中主要就是向容器中添加我们常用的IOptions<TOptions>
、IOptionsSnapshot<TOptions>
、IOptionsMonitor<TOptions>
服务接口,这里也可以看到不同服务接口对于的生命周期。除此之外还有工厂服务IOptionsFactory<>
和缓存服务IOptionsMonitorCache<>
,这两个就是选项系统的关键。每个选项进行配置的时候都会同时注入这些服务,所以每一个选项我们都能使用三个不同接口去解析。
OptionsBuilder
上面第二个 AddOptions 方法返回 OptionsBuilder<TOptions>
对象。之前讲过 OptionsBuilder<TOptions>
类中也有 Configure 方法,其实不止 Configure 方法,其他的 PostConfigure 方法等也有,它其实就是最终的选项系统配置类,我们所有的选项配置其实都可以通过调用第二个 AddOptions 方法,再通过 OptionsBuilder<TOptions>
对象中的方法来完成配置。其他各个扩展方法的配置方式不过是进行了使用简化而已。
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<TOptions>
方法进行添加的。
验证规则配置有三种方式,最后其实都是通过 IValidateOptions<TOptions>
的实现类来完成。我们自己实现的自定义验证类就不用说了,最后我们会将其注入到容器中,而从上面的代码中可以看到,当我们通过委托的方式自定义验证规则的时候,它会被构建成一个 ValidateOptions 类对象,并注入到容器中作为一个服务。
ValidateOptions<TOptions>
是 IValidateOptions 的一个实现类,构造函数中接收委托,通过委托返回的 bool 结果判断验证是否通过。
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 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<TOptions>
、UnnamedOptionsManager<TOptions>
接下来看选项使用相关的内容,其中 IOptions<TOptions>
中的选项类一经创建一直保持不变,默认实现类 UnnamedOptionsManager<TOptions>
。
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<TOptions>
接口只有一个 Value 属性,实现类中通过锁确保创建的 Value 值不会因为线程问题导致不同,且该服务被注册为单例生命周期,所以对象不销毁,后续一直会读取内存中的 Value 值。具体选项类对象的创建由工厂服务负责。
IOptionsSnapshot<TOptions>
、OptionsManager<TOptions>
IOptionsSnapshot<TOptions>
的实现类是 OptionsManager<TOptions>
,该类中有一个私有的 OptionsCache<TOptions>
属性,每次对选项类进行读取的时候,都是先尝试从缓存读取,如果没有才创建。而由于 IOptionsSnapshot<TOptions>
被注册为请求域生命周期,所以单次请求内相应对象不会销毁,缓存不会清空,会一直保持一个。
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<TOptions>
、OptionsMonitor<TOptions>
IOptionsMonitor<TOptions>
每次获取选项类都是最新的值,它实现类是 OptionsMonitor<TOptions>
,实现类中使用了从容器中注入的单例缓存 IOptionsMonitorCache<TOptions>
来保存选项类,并且通过相应的 IOptionsChangeTokenSource<TOptions>
注册了选项类绑定内容的监听,例如上面讲到的 ConfigurationChangeTokenSource<TOptions>
,在选项类配置内容改变的时候会触发事件,而在事件中会将缓存清空并重新获取创建类,并且执行注册进来的额外的监听事件,可以看看下面的 InvokeChanged 方法。
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptionsMonitor<TOptions>,IDisposablewhere 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 tokensforeach (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<TOptions>
和 OptionsSnapshot<TOptions>
都使用到了这个,OptionsSnapshot<TOptions>
通过内部创建的私有的缓存属性实现了请求域内选项类不变,而 OptionsMonitor<TOptions>
则通过它减少了每次都直接读取配置来源(如文件、数据库、配置中心api)的性能消耗,而是通过变更事件的方式进行更新。其实我们还可以在需要的时候注入IOptionsMonitorCache 服务自行对选项类进行更新。
OptionsCache 的具体实现比较简单,主要就是通过 ConcurrentDictionary<string, Lazy<TOptions>>
对象作为内存缓存,其中为了性能还再使用了 Lazy 方式。
IOptionsFactory<TOptions>
、OptionsFactory<TOptions>
OptionsFactory<TOptions>
类实现 IOptionsFactory<TOptions>
接口,是选项类的实际创建配置之处,其实就是将之前注册到容器中与当前相关的各种配置、验证的行为配置类注入进来,再通过反射创建对象之后,将选项类对象传进去,逐一对相应的行为进行调用,最后得到一个成型的选项类。这里选项类的创建方式很简单,这也是要求选项类要有无参构造函数的原因。
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 - 选项(Options) ,作者概括得很好,这里就直接引用了,文章中的一些内容也是借鉴了它。
6. 总结
- 所有选项均为命名选项,默认名称为Options.DefaultName,即string.Empty。
- 通过ConfigurationBinder.Get或ConfigurationBinder.Bind手动获取选项实例。
- 通过Configure方法进行选项配置:
OptionsBuilder<TOptions>
.Configure:通过包含DI服务的委托来进行选项配置OptionsServiceCollectionExtensions.Configure<TOptions>
:通过简单委托来进行选项配置OptionsConfigurationServiceCollectionExtensions.Configure<TOptions>
:直接将IConfiguration实例绑定到选项上
- 通过
OptionsServiceCollectionExtensions.ConfigureAll<TOptions>
方法针对某个选项类型的所有实例(不同名称)统一进行配置。 - 通过PostConfigure方法进行选项后期配置:
OptionsBuilder<TOptions>
.PostConfigure:通过包含DI服务的委托来进行选项后期配置OptionsServiceCollectionExtensions.PostConfigure<TOptions>
:通过简单委托来进行选项后期配置
- 通过
PostConfigureAll<TOptions>
方法针对某个选项类型的所有实例(不同名称)统一进行配置。 - 通过Validate进行选项验证:
- OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations:通过数据注解进行选项验证
OptionsBuilder<TOptions>
.Validate:通过委托进行选项验证IValidateOptions<TOptions>
:通过实现该接口并注入实现来进行选项验证
- 通过依赖注入读取选项:
IOptions<TOptions>
:Singleton,值永远是该接口被实例化时的选项配置初始值IOptionsSnapshot<TOptions>
:Scoped,每一次Http请求开始时会读取选项配置的最新值,并在当前请求中保持不变IOptionsMonitor<TOptions>
:Singleton,每次读取都是选项配置的最新值
参考文章:
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 — 缓存之内存缓存(上)