AI智能
改变未来

理解ASP.NET Core – 选项(Options)

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

Options绑定

上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊

Options

,中文译为“选项”,该功能用于实现以强类型的方式对程序配置信息进行访问。

既然是强类型的方式,那么就需要定义一个Options类,该类:

  • 推荐命名规则:
    {Object}Options
  • 特点:非抽象类
  • 必须包含公共无参的构造函数
  • 类中的所有公共读写属性都会与配置项进行绑定
  • 字段不会被绑定

接下来,为了便于理解,先举个例子:

首先在 appsetting.json 中添加如下配置:

{"Book": {"Id": 1,"Name": "三国演义","Author": "罗贯中"}}

然后定义Options类:

public class BookOptions{public const string Book = "Book";public int Id { get; set; }public string Name { get; set; }public string Author { get; set; }}

最后进行绑定(有

Bind

Get

两种方式):

public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){// 方式 1:var bookOptions1 = new BookOptions();Configuration.GetSection(BookOptions.Book).Bind(bookOptions1);// 方式 2:var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();}}

其中,属性

Id

Title

Author

均会与配置进行绑定,但是字段

Book

并不会被绑定,该字段只是用来让我们避免在程序中使用“魔数”。另外,一定要确保配置项能够转换到其绑定的属性类型(你该不会想把

string

绑定到

int

类型上吧)。

如果中文读取出来是乱码,那么你可以按照.L的.net core 读取appsettings.json 文件中文乱码的问题来配置一下。

当然,这样写代码还不够完美,还是要将Options添加到依赖注入服务容器中,例如通过

IServiceCollection

的扩展方法

Configure

public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));}}

Options读取

通过Options接口,我们可以读取依赖注入容器中的Options。常用的有三个接口:

  • IOptions<TOptions>
  • IOptionsSnapshot<TOptions>
  • IOptionsMonitor<TOptions>

接下来,我们看看它们的区别。

IOptions

  • 该接口对象实例生命周期为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 当该接口被实例化后,其中的选项值将永远保持不变,即使后续修改了与选项进行绑定的配置,也永远读取不到修改后的配置值
  • 不支持命名选项(Named Options),这个下面会说
public class ValuesController : ControllerBase{private readonly BookOptions _bookOptions;public ValuesController(IOptions<BookOptions> bookOptions){// bookOptions.Value 始终是程序启动时加载的配置,永远不会改变_bookOptions = bookOptions.Value;}}

IOptionsSnapshot

  • 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。
  • 在作用域中,创建
    IOptionsSnapshot<TOptions>

    对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。

  • 支持命名选项
public class ValuesController : ControllerBase{private readonly BookOptions _bookOptions;public ValuesController(IOptionsSnapshot<BookOptions> bookOptionsSnapshot){// bookOptions.Value 是 Options 对象实例创建时读取的配置快照_bookOptions = bookOptionsSnapshot.Value;}}

IOptionsMonitor

  • 该接口除了可以查看
    TOptions

    的值,还可以监控

    TOptions

    配置的更改。

  • 该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 每次读取选项值时,都是从配置中读取最新选项值(具体读取逻辑查看下方三种接口对比测试)。
  • 支持:命名选项
  • 重新加载配置(
    CurrentValue

    ),并当配置发生更改时,进行通知(

    OnChange

  • 缓存与缓存失效 (
    IOptionsMonitorCache<TOptions>

    )

public class ValuesController : ControllerBase{private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;public ValuesController(IOptionsMonitor<BookOptions> bookOptionsMonitor){// _bookOptionsMonitor.CurrentValue 的值始终是最新配置的值_bookOptionsMonitor = bookOptionsMonitor;}}

三种接口对比测试

IOptions<TOptions>

就不说了,主要说一下

IOptionsSnapshot<TOptions>

IOptionsMonitor<TOptions>

的不同:

  • IOptionsSnapshot<TOptions>

    注册为 Scoped,在创建其实例时,会从配置中读取最新选项值作为快照,并在作用域中使用该快照

  • IOptionsMonitor<TOptions>

    注册为 Singleton,每次调用实例的 CurrentValue 时,会先检查缓存(

    IOptionsMonitorCache<TOptions>

    )是否有值,如果有值,则直接用,如果没有,则从配置中读取最新选项值,并记入缓存。当配置发生更改时,会将缓存清空。

搞个测试小程序:

[ApiController][Route("[controller]")]public class ValuesController : ControllerBase{private readonly IOptions<BookOptions> _bookOptions;private readonly IOptionsSnapshot<BookOptions> _bookOptionsSnapshot;private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;public ValuesController(IOptions<BookOptions> bookOptions,IOptionsSnapshot<BookOptions> bookOptionsSnapshot,IOptionsMonitor<BookOptions> bookOptionsMonitor){_bookOptions = bookOptions;_bookOptionsSnapshot = bookOptionsSnapshot;_bookOptionsMonitor = bookOptionsMonitor;}[HttpGet]public dynamic Get(){var bookOptionsValue1 = _bookOptions.Value;var bookOptionsSnapshotValue1 = _bookOptionsSnapshot.Value;var bookOptionsMonitorValue1 = _bookOptionsMonitor.CurrentValue;Console.WriteLine("请修改配置文件 appsettings.json");Task.Delay(TimeSpan.FromSeconds(10)).Wait();var bookOptionsValue2 = _bookOptions.Value;var bookOptionsSnapshotValue2 = _bookOptionsSnapshot.Value;var bookOptionsMonitorValue2 = _bookOptionsMonitor.CurrentValue;return new{bookOptionsValue1,bookOptionsSnapshotValue1,bookOptionsMonitorValue1,bookOptionsValue2,bookOptionsSnapshotValue2,bookOptionsMonitorValue2};}}

运行2次,并按照指示修改两次配置文件(初始是“三国演义”,第一次修改为“水浒传”,第二次修改为“红楼梦”)

  • 第1次输出:
{"bookOptionsValue1": {"id": 1,"name": "三国演义","author": "罗贯中"},"bookOptionsSnapshotValue1": {"id": 1,"name": "三国演义","author": "罗贯中"},"bookOptionsMonitorValue1": {"id": 1,"name": "三国演义","author": "罗贯中"},"bookOptionsValue2": {"id": 1,"name": "三国演义","author": "罗贯中"},// 注意 OptionsSnapshot 的值在当前作用域内没有进行更新"bookOptionsSnapshotValue2": {"id": 1,"name": "三国演义","author": "罗贯中"},// 注意 OptionsMonitor 的值变成最新的"bookOptionsMonitorValue2": {"id": 1,"name": "水浒传","author": "施耐庵"}}
  • 第2次输出:
{// Options 的值始终没有变化"bookOptionsValue1": {"id": 1,"name": "三国演义","author": "罗贯中"},// 注意 OptionsSnapshot 的值变成当前最新值了"bookOptionsSnapshotValue1": {"id": 1,"name": "水浒传","author": "施耐庵"},// 注意 OptionsMonitor 的值始终是最新的"bookOptionsMonitorValue1": {"id": 1,"name": "水浒传","author": "施耐庵"},// Options 的值始终没有变化"bookOptionsValue2": {"id": 1,"name": "三国演义","author": "罗贯中"},// 注意 OptionsSnapshot 的值在当前作用域内没有进行更新"bookOptionsSnapshotValue2": {"id": 1,"name": "水浒传","author": "施耐庵"},// 注意 OptionsMonitor 的值始终是最新的"bookOptionsMonitorValue2": {"id": 1,"name": "红楼梦","author": "曹雪芹"}}

通过测试我相信你应该能深刻理解它们之间的区别了。

命名选项(Named Options)

上面我们提到了命名选项,命名选项常用于多个配置节点绑定同一属性的情况,举个例子你就明白了:

在 appsettings.json 中添加如下配置

{"DateTime": {"Beijing": {"Year": 2021,"Month": 1,"Day":1,"Hour":12,"Minute":0,"Second":0},"Tokyo": {"Year": 2021,"Month": 1,"Day":1,"Hour":13,"Minute":0,"Second":0},}}

很显然,虽然“Beijing”和“Tokyo”是两个配置项,但是属性都是一样的,我们没必要创建两个Options类,只需要创建一个就好了:

public class DateTimeOptions{public const string Beijing = "Beijing";public const string Tokyo = "Tokyo";public int Year { get; set; }public int Month { get; set; }public int Day { get; set; }public int Hour { get; set; }public int Minute { get; set; }public int Second { get; set; }}

然后,通过对选项进行指定命名的方式,一个叫做“Beijing”,一个叫做“Tokyo”,将选项添加到DI容器中:

public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }public void ConfigureServices(IServiceCollection services){services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));services.Configure<DateTimeOptions>(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}"));services.Configure<DateTimeOptions>(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}"));}}

最后,通过构造函数的方式将选项注入到Controller中。需要注意的是,因为

DateTimeOptions

类绑定了两个选项类,所以当我们获取时选项值时,需要指定选项的名字。

public class ValuesController : ControllerBase{private readonly DateTimeOptions _beijingDateTimeOptions;private readonly DateTimeOptions _tockyoDateTimeOptions;public ValuesController(IOptionsSnapshot<DateTimeOptions> dateTimeOptions){_beijingDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Beijing);_tockyoDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Tokyo);}}

程序运行后,你会发现变量 _beijingDateTimeOptions 绑定的配置是“Beijing”配置节点,变量 _tockyoDateTimeOptions 绑定的配置是“Tokyo” 配置节点,但它们绑定的都是同一个类

DateTimeOptions

事实上,.NET Core 中所有 Options 都是命名选项,当没有显式指定名字时,使用的名字默认是

Options.DefaultName

,即

string.Empty

使用 DI 服务配置选项

在某些场景下,选项的配置需要依赖DI中的服务,这时可以借助OptionsBuilder的

Configure

方法(注意这个

Configure

不是上面提到的

IServiceCollection

的扩展方法

Configure

,这是两个不同的方法),该方法支持最多5个服务来配置选项:

services.AddOptions<BookOptions>().Configure<Service1, Service2, Service3, Service4, Service5>((o, s, s2, s3, s4, s5) =>{o.Authors = DoSomethingWith(s, s2, s3, s4, s5);});

Options 验证

配置毕竟是我们手动进行文本输入的,难免会出现错误,这种情况下,就需要使用程序来帮助进行校验了。

DataAnnotations

Install-Package Microsoft.Extensions.Options.DataAnnotations

我们先升级一下

BookOptions

,增加一些数据校验:

public class BookOptions{public const string Book = "Book";[Range(1,1000,ErrorMessage = "必须 {1} <= {0} <= {2}")]public int Id { get; set; }[StringLength(10, MinimumLength = 1,ErrorMessage = "必须 {2} <= {0} Length <= {1}")]public string Name { get; set; }public string Author { get; set; }}

然后我们在添加到DI容器时,增加数据注解验证:

public void ConfigureServices(IServiceCollection services){services.AddOptions<BookOptions>().Bind(Configuration.GetSection(BookOptions.Book)).ValidateDataAnnotations();.Validate(options =>{// 校验通过 return true// 校验失败 return falseif (options.Author.Contains("A")){return false;}return true;});}

ValidateDataAnnotations

会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用

Validate

进行较为复杂的校验,如果过于复杂,则就要用到

IValidateOptions

了(实质上,

Validate

方法内部也是通过注入一个

IValidateOptions

实例来实现选项验证的)。

IValidateOptions

通过实现

IValidateOptions<TOptions>

接口,增加数据校验规则,例如:

public class BookValidation : IValidateOptions<BookOptions>{public ValidateOptionsResult Validate(string name, BookOptions options){var failures = new List<string>();if(!(options.Id >= 1 && options.Id <= 1000)){failures.Add($"必须 1 <= {nameof(options.Id)} <= {1000}");}if(!(options.Name.Length >= 1 && options.Name.Length <= 10)){failures.Add($"必须 1 <= {nameof(options.Name)} <= 10");}if (failures.Any()){return ValidateOptionsResult.Fail(failures);}return ValidateOptionsResult.Success;}}

然后我们将其注入到DI容器 Singleton,这里使用了

TryAddEnumerable

扩展方法添加该服务,是因为我们可以注入多个针对同一Options的

IValidateOptions

,这些

IValidateOptions

实例都会被执行:

public void ConfigureServices(IServiceCollection services){services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<BookOptions>, BookValidation>());}

Options后期配置

介绍两个方法,分别是

PostConfigure

PostConfigureAll

,他们用来对选项进行后期配置。

  • 在所有的
    OptionsServiceCollectionExtensions.Configure

    方法运行后执行

  • Configure

    ConfigureAll

    类似,

    PostConfigure

    仅用于对指定名称的选项进行后期配置(默认名称为

    string.Empty

    ),

    PostConfigureAll

    则用于对所有选项实例进行后期配置

  • 每当选项更改时,均会触发相应的方法
public void ConfigureServices(IServiceCollection services){services.PostConfigure<DateTimeOptions>(options =>{Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置");});services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>{Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置");});services.PostConfigureAll<DateTimeOptions>(options =>{Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置");});}

Options 体系

IConfigureOptions

该接口用于包装对

选项

的配置。默认实现为

ConfigureOptions<TOptions>

public interface IConfigureOptions<in TOptions> where TOptions : class{void Configure(TOptions options);}

ConfigureOptions

public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class{public ConfigureOptions(Action<TOptions> action){Action = action;}public Action<TOptions> Action { get; }// 配置 TOptions 实例public virtual void Configure(TOptions options){Action?.Invoke(options);}}

ConfigureFromConfigurationOptions

该类通过继承类

ConfigureOptions<TOptions>

,对选项的配置进行了扩展,允许通过

ConfigurationBinder.Bind

扩展方法将

IConfiguration

实例绑定到选项上:

public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>where TOptions : class{public ConfigureFromConfigurationOptions(IConfiguration config): base(options => ConfigurationBinder.Bind(config, options)){ }}

IConfigureNamedOptions

该接口用于包装对

命名选项

的配置,该接口同时继承了接口

IConfigureOptions<TOptions>

的行为,默认实现为

ConfigureNamedOptions<TOptions>

,另外为了实现“使用 DI 服务配置选项”的功能,还提供了一些泛型类重载。

public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class{void Configure(string name, TOptions options);}

ConfigureNamedOptions

public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class{public ConfigureNamedOptions(string name, Action<TOptions> action){Name = name;Action = action;}public string Name { get; }public Action<TOptions> Action { get; }public virtual void Configure(string name, TOptions options){// Name == null 表示针对 TOptions 的所有实例进行配置if (Name == null || name == Name){Action?.Invoke(options);}}public void Configure(TOptions options) => Configure(Options.DefaultName, options);}

NamedConfigureFromConfigurationOptions

该类通过继承类

ConfigureNamedOptions<TOptions>

,对命名选项的配置进行了扩展,允许通过

ConfigurationBinder.Bind

扩展方法将

IConfiguration

实例绑定到命名选项上:

public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>where TOptions : class{public NamedConfigureFromConfigurationOptions(string name, IConfiguration config): this(name, config, _ => { }){ }public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder): base(name, options => config.Bind(options, configureBinder)){ }}

IPostConfigureOptions

该接口用于包装对

命名选项

的后期配置,将在所有

IConfigureOptions<TOptions>

执行完毕后才会执行,默认实现为

PostConfigureOptions<TOptions>

,同样的,为了实现“使用 DI 服务对选项进行后期配置”的功能,也提供了一些泛型类重载:

public interface IPostConfigureOptions<in TOptions> where TOptions : class{void PostConfigure(string name, TOptions options);}public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class{public PostConfigureOptions(string name, Action<TOptions> action){Name = name;Action = action;}public string Name { get; }public Action<TOptions> Action { get; }public virtual void PostConfigure(string name, TOptions options){// Name == null 表示针对 TOptions 的所有实例进行后期配置if (Name == null || name == Name){Action?.Invoke(options);}}}

AddOptions & AddOptions & OptionsBuilder

public static class OptionsServiceCollectionExtensions{// 该方法帮我们把一些常用的与 Options 相关的服务注入到 DI 容器public static IServiceCollection AddOptions(this IServiceCollection 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;}// 没有指定 Options 名称时,默认使用 Options.DefaultNamepublic static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class=> services.AddOptions<TOptions>(Options.Options.DefaultName);// 由于后续还要对 TOptions 进行配置,所以返回一个 OptionsBuilder 出去public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)where TOptions : class{services.AddOptions();return new OptionsBuilder<TOptions>(services, name);}}

那我们看看

OptionsBuilder<TOptions>

可以配置哪些东西,由于该类中有大量重载方法,我只挑选最基础的方法来看一看:

public class OptionsBuilder<TOptions> where TOptions : class{private const string DefaultValidationFailureMessage = "A validation error has occurred.";// TOptions 实例的名字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;}}

OptionsServiceCollectionExtensions.Configure

OptionsServiceCollectionExtensions.Configure<TOptions>

实际上就是对选项的一般配置方式进行了封装,免去了

OptionsBuilder<TOptions>

public static class OptionsServiceCollectionExtensions{// 没有指定 Options 名称时,默认使用 Options.DefaultNamepublic static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class=> services.Configure(Options.Options.DefaultName, configureOptions);// 等同于做了 AddOptions<TOptions> 和 OptionsBuilder<TOptions>.Configure 两件事public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)where TOptions : class{services.AddOptions();services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));return services;}// 由于 ConfigureAll 是针对 TOptions 的所有实例进行配置,所以不需要指定名字public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class=> services.Configure(name: null, configureOptions: configureOptions);}

OptionsConfigurationServiceCollectionExtensions.Configure

请注意,该

Configure<TOptions>

方法与上方提及的

Configure<TOptions>

不是同一个。该扩展方法针对配置(IConfiguration)绑定到选项(Options)上进行了扩展

Install-Package Microsoft.Extensions.Options.ConfigurationExtensions

public static class OptionsConfigurationServiceCollectionExtensions{public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class=> services.Configure<TOptions>(Options.Options.DefaultName, config);public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class=> services.Configure<TOptions>(name, config, _ => { });public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)where TOptions : class=> services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)where TOptions : class{services.AddOptions();services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));}}

IOptionsFactory

IOptionsFactory<TOptions>

负责创建

命名选项

实例,默认实现为

OptionsFactory<TOptions>

public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class{TOptions Create(string name);}public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions>: IOptionsFactory<TOptions> where TOptions : class{private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;private readonly IEnumerable<IValidateOptions<TOptions>> _validations;// 这里通过依赖注入的的方式将与 TOptions 相关的配置、验证服务列表解析出来public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures): this(setups, postConfigures, validations: null){ }public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations){_setups = setups;_postConfigures = postConfigures;_validations = validations;}public TOptions Create(string name){// 1. 创建并配置 OptionsTOptions 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);}}// 2. 对 Options 进行后期配置foreach (IPostConfigureOptions<TOptions> post in _postConfigures){post.PostConfigure(name, options);}// 3. 执行 Options 校验if (_validations != null){var failures = new List<string>();foreach (IValidateOptions<TOptions> validate in _validations){ValidateOptionsResult result = validate.Validate(name, options);if (result.Failed){failures.AddRange(result.Failures);}}if (failures.Count > 0){throw new OptionsValidationException(name, typeof(TOptions), failures);}}return options;}protected virtual TOptions CreateInstance(string name){return Activator.CreateInstance<TOptions>();}}

OptionsManager

通过

AddOptions

扩展方法的实现,可以看到,

IOptions<TOptions>

IOptionsSnapshot<TOptions>

的实现都是

OptionsManager<TOptions>

,只不过一个是 Singleton,一个是 Scoped。我们通过前面的分析也知道了,当源中的配置改变时,

IOptions<TOptions>

始终维持初始值,

IOptionsSnapshot<TOptions>

在每次请求时会读取最新配置值,并在同一个请求中是不变的。接下来就来看看

OptionsManager<TOptions>

是如何实现的:

public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptions<TOptions>,IOptionsSnapshot<TOptions>where TOptions : class{private readonly IOptionsFactory<TOptions> _factory;// 将已创建的 TOptions 实例缓存到该私有变量中private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>();public OptionsManager(IOptionsFactory<TOptions> factory){_factory = factory;}public TOptions Value => Get(Options.DefaultName);public virtual TOptions Get(string name){name = name ?? Options.DefaultName;// 若缓存不存在,则通过工厂新建 Options 实例,否则直接读取缓存return _cache.GetOrAdd(name, () => _factory.Create(name));}}

OptionsMonitor

同样,通过前面的分析,我们知道

OptionsMonitor<TOptions>

读取的始终是配置的最新值,它的实现在

OptionsManager<TOptions>

的基础上,除了使用缓存将创建的 Options 实例缓存起来外,还增添了监听机制,当配置发生更改时,会将缓存移除。

public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :IOptionsMonitor<TOptions>,IDisposablewhere TOptions : class{private readonly IOptionsMonitorCache<TOptions> _cache;private readonly IOptionsFactory<TOptions> _factory;private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;private readonly List<IDisposable> _registrations = new List<IDisposable>();internal event Action<TOptions, string> _onChange;public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache){_factory = factory;_sources = sources;_cache = cache;// 监听更改foreach (IOptionsChangeTokenSource<TOptions> source in _sources){IDisposable registration = ChangeToken.OnChange(() => source.GetChangeToken(),(name) => InvokeChanged(name),source.Name);_registrations.Add(registration);}}// 当发生更改时,移除缓存private void InvokeChanged(string name){name = name ?? Options.DefaultName;_cache.TryRemove(name);TOptions options = Get(name);if (_onChange != null){_onChange.Invoke(options, name);}}public TOptions CurrentValue => Get(Options.DefaultName);public virtual TOptions Get(string name){name = name ?? Options.DefaultName;return _cache.GetOrAdd(name, () => _factory.Create(name));}// 通过该方法绑定 OnChange 事件public IDisposable OnChange(Action<TOptions, string> listener){var disposable = new ChangeTrackerDisposable(this, listener);_onChange += disposable.OnChange;return disposable;}public void Dispose(){// 移除所有 change token 的订阅foreach (IDisposable registration in _registrations){registration.Dispose();}_registrations.Clear();}}

总结

  • 所有选项均为命名选项,默认名称为
    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,每次读取都是选项配置的最新值

  • 赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » 理解ASP.NET Core – 选项(Options)