.NetCore 小笨蛋 发布于:2022年09月26日 更新于:2022年09月26日 121

Options绑定

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

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

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

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

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

  1. {
  2. "Book": {
  3. "Id": 1,
  4. "Name": "三国演义",
  5. "Author": "罗贯中"
  6. }
  7. }

然后定义Options类:

  1. public class BookOptions
  2. {
  3. public const string Book = "Book";
  4. public int Id { get; set; }
  5. public string Name { get; set; }
  6. public string Author { get; set; }
  7. }

最后进行绑定(有Bind和Get两种方式):

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. // 方式 1:
  11. var bookOptions1 = new BookOptions();
  12. Configuration.GetSection(BookOptions.Book).Bind(bookOptions1);
  13. // 方式 2:
  14. var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
  15. }
  16. }

其中,属性Id、Title、Author均会与配置进行绑定,但是字段Book并不会被绑定,该字段只是用来让我们避免在程序中使用“魔数”。另外,一定要确保配置项能够转换到其绑定的属性类型(你该不会想把string绑定到int类型上吧)。

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
  11. }
  12. }

Options读取

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

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

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

IOptions

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

IOptionsSnapshot

  • 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。
  • 在作用域中,创建IOptionsSnapshot对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。
  • 支持命名选项
  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly BookOptions _bookOptions;
  4. public ValuesController(IOptionsSnapshot<BookOptions> bookOptionsSnapshot)
  5. {
  6. // bookOptions.Value 是 Options 对象实例创建时读取的配置快照
  7. _bookOptions = bookOptionsSnapshot.Value;
  8. }
  9. }

IOptionsMonitor

  • 该接口除了可以查看TOptions的值,还可以监控TOptions配置的更改。
  • 该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 每次读取选项值时,都是从配置中读取最新选项值(具体读取逻辑查看下方三种接口对比测试)。
  • 支持:
    • 命名选项
    • 重新加载配置(CurrentValue),并当配置发生更改时,进行通知(OnChange)
    • 缓存与缓存失效 (IOptionsMonitorCache)
  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;
  4. public ValuesController(IOptionsMonitor<BookOptions> bookOptionsMonitor)
  5. {
  6. // _bookOptionsMonitor.CurrentValue 的值始终是最新配置的值
  7. _bookOptionsMonitor = bookOptionsMonitor;
  8. }
  9. }

三种接口对比测试

IOptions就不说了,主要说一下IOptionsSnapshot和IOptionsMonitor的不同:

  • IOptionsSnapshot 注册为 Scoped,在创建其实例时,会从配置中读取最新选项值作为快照,并在作用域中使用该快照
  • IOptionsMonitor 注册为 Singleton,每次调用实例的 CurrentValue 时,会先检查缓存(IOptionsMonitorCache)是否有值,如果有值,则直接用,如果没有,则从配置中读取最新选项值,并记入缓存。当配置发生更改时,会将缓存清空。

搞个测试小程序:

  1. [ApiController]
  2. [Route("[controller]")]
  3. public class ValuesController : ControllerBase
  4. {
  5. private readonly IOptions<BookOptions> _bookOptions;
  6. private readonly IOptionsSnapshot<BookOptions> _bookOptionsSnapshot;
  7. private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;
  8. public ValuesController(
  9. IOptions<BookOptions> bookOptions,
  10. IOptionsSnapshot<BookOptions> bookOptionsSnapshot,
  11. IOptionsMonitor<BookOptions> bookOptionsMonitor)
  12. {
  13. _bookOptions = bookOptions;
  14. _bookOptionsSnapshot = bookOptionsSnapshot;
  15. _bookOptionsMonitor = bookOptionsMonitor;
  16. }
  17. [HttpGet]
  18. public dynamic Get()
  19. {
  20. var bookOptionsValue1 = _bookOptions.Value;
  21. var bookOptionsSnapshotValue1 = _bookOptionsSnapshot.Value;
  22. var bookOptionsMonitorValue1 = _bookOptionsMonitor.CurrentValue;
  23. Console.WriteLine("请修改配置文件 appsettings.json");
  24. Task.Delay(TimeSpan.FromSeconds(10)).Wait();
  25. var bookOptionsValue2 = _bookOptions.Value;
  26. var bookOptionsSnapshotValue2 = _bookOptionsSnapshot.Value;
  27. var bookOptionsMonitorValue2 = _bookOptionsMonitor.CurrentValue;
  28. return new
  29. {
  30. bookOptionsValue1,
  31. bookOptionsSnapshotValue1,
  32. bookOptionsMonitorValue1,
  33. bookOptionsValue2,
  34. bookOptionsSnapshotValue2,
  35. bookOptionsMonitorValue2
  36. };
  37. }
  38. }

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

  • 第1次输出:

    1. {
    2. "bookOptionsValue1": {
    3. "id": 1,
    4. "name": "三国演义",
    5. "author": "罗贯中"
    6. },
    7. "bookOptionsSnapshotValue1": {
    8. "id": 1,
    9. "name": "三国演义",
    10. "author": "罗贯中"
    11. },
    12. "bookOptionsMonitorValue1": {
    13. "id": 1,
    14. "name": "三国演义",
    15. "author": "罗贯中"
    16. },
    17. "bookOptionsValue2": {
    18. "id": 1,
    19. "name": "三国演义",
    20. "author": "罗贯中"
    21. },
    22. // 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
    23. "bookOptionsSnapshotValue2": {
    24. "id": 1,
    25. "name": "三国演义",
    26. "author": "罗贯中"
    27. },
    28. // 注意 OptionsMonitor 的值变成最新的
    29. "bookOptionsMonitorValue2": {
    30. "id": 1,
    31. "name": "水浒传",
    32. "author": "施耐庵"
    33. }
    34. }
  • 第2次输出:

    1. {
    2. // Options 的值始终没有变化
    3. "bookOptionsValue1": {
    4. "id": 1,
    5. "name": "三国演义",
    6. "author": "罗贯中"
    7. },
    8. // 注意 OptionsSnapshot 的值变成当前最新值了
    9. "bookOptionsSnapshotValue1": {
    10. "id": 1,
    11. "name": "水浒传",
    12. "author": "施耐庵"
    13. },
    14. // 注意 OptionsMonitor 的值始终是最新的
    15. "bookOptionsMonitorValue1": {
    16. "id": 1,
    17. "name": "水浒传",
    18. "author": "施耐庵"
    19. },
    20. // Options 的值始终没有变化
    21. "bookOptionsValue2": {
    22. "id": 1,
    23. "name": "三国演义",
    24. "author": "罗贯中"
    25. },
    26. // 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
    27. "bookOptionsSnapshotValue2": {
    28. "id": 1,
    29. "name": "水浒传",
    30. "author": "施耐庵"
    31. },
    32. // 注意 OptionsMonitor 的值始终是最新的
    33. "bookOptionsMonitorValue2": {
    34. "id": 1,
    35. "name": "红楼梦",
    36. "author": "曹雪芹"
    37. }
    38. }

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

命名选项(Named Options)

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

在 appsettings.json 中添加如下配置

  1. {
  2. "DateTime": {
  3. "Beijing": {
  4. "Year": 2021,
  5. "Month": 1,
  6. "Day":1,
  7. "Hour":12,
  8. "Minute":0,
  9. "Second":0
  10. },
  11. "Tokyo": {
  12. "Year": 2021,
  13. "Month": 1,
  14. "Day":1,
  15. "Hour":13,
  16. "Minute":0,
  17. "Second":0
  18. },
  19. }
  20. }

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

  1. public class DateTimeOptions
  2. {
  3. public const string Beijing = "Beijing";
  4. public const string Tokyo = "Tokyo";
  5. public int Year { get; set; }
  6. public int Month { get; set; }
  7. public int Day { get; set; }
  8. public int Hour { get; set; }
  9. public int Minute { get; set; }
  10. public int Second { get; set; }
  11. }

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
  11. services.Configure<DateTimeOptions>(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}"));
  12. services.Configure<DateTimeOptions>(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}"));
  13. }
  14. }

最后,通过构造函数的方式将选项注入到Controller中。需要注意的是,因为DateTimeOptions类绑定了两个选项类,所以当我们获取时选项值时,需要指定选项的名字。

  1. public class ValuesController : ControllerBase
  2. {
  3. private readonly DateTimeOptions _beijingDateTimeOptions;
  4. private readonly DateTimeOptions _tockyoDateTimeOptions;
  5. public ValuesController(IOptionsSnapshot<DateTimeOptions> dateTimeOptions)
  6. {
  7. _beijingDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Beijing);
  8. _tockyoDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Tokyo);
  9. }
  10. }

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

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

使用 DI 服务配置选项

在某些场景下,选项的配置需要依赖DI中的服务,这时可以借助OptionsBuilder的Configure方法(注意这个Configure不是上面提到的IServiceCollection的扩展方法Configure,这是两个不同的方法),该方法支持最多5个服务来配置选项:

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

Options 验证

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

DataAnnotations

Install-Package Microsoft.Extensions.Options.DataAnnotations

我们先升级一下BookOptions,增加一些数据校验:

  1. public class BookOptions
  2. {
  3. public const string Book = "Book";
  4. [Range(1,1000,
  5. ErrorMessage = "必须 {1} <= {0} <= {2}")]
  6. public int Id { get; set; }
  7. [StringLength(10, MinimumLength = 1,
  8. ErrorMessage = "必须 {2} <= {0} Length <= {1}")]
  9. public string Name { get; set; }
  10. public string Author { get; set; }
  11. }

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddOptions<BookOptions>()
  4. .Bind(Configuration.GetSection(BookOptions.Book))
  5. .ValidateDataAnnotations();
  6. .Validate(options =>
  7. {
  8. // 校验通过 return true
  9. // 校验失败 return false
  10. if (options.Author.Contains("A"))
  11. {
  12. return false;
  13. }
  14. return true;
  15. });
  16. }

ValidateDataAnnotations会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用Validate进行较为复杂的校验,如果过于复杂,则就要用到IValidateOptions了(实质上,Validate方法内部也是通过注入一个IValidateOptions实例来实现选项验证的)。

IValidateOptions

通过实现IValidateOptions接口,增加数据校验规则,例如:

  1. public class BookValidation : IValidateOptions<BookOptions>
  2. {
  3. public ValidateOptionsResult Validate(string name, BookOptions options)
  4. {
  5. var failures = new List<string>();
  6. if(!(options.Id >= 1 && options.Id <= 1000))
  7. {
  8. failures.Add($"必须 1 <= {nameof(options.Id)} <= {1000}");
  9. }
  10. if(!(options.Name.Length >= 1 && options.Name.Length <= 10))
  11. {
  12. failures.Add($"必须 1 <= {nameof(options.Name)} <= 10");
  13. }
  14. if (failures.Any())
  15. {
  16. return ValidateOptionsResult.Fail(failures);
  17. }
  18. return ValidateOptionsResult.Success;
  19. }
  20. }

然后我们将其注入到DI容器 Singleton,这里使用了TryAddEnumerable扩展方法添加该服务,是因为我们可以注入多个针对同一Options的IValidateOptions,这些IValidateOptions实例都会被执行:

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

Options后期配置

介绍两个方法,分别是PostConfigure和PostConfigureAll,他们用来对选项进行后期配置。

  • 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行
  • 与Configure和ConfigureAll类似,PostConfigure仅用于对指定名称的选项进行后期配置(默认名称为string.Empty),PostConfigureAll则用于对所有选项实例进行后期配置
  • 每当选项更改时,均会触发相应的方法
  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.PostConfigure<DateTimeOptions>(options =>
  4. {
  5. Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置");
  6. });
  7. services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>
  8. {
  9. Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置");
  10. });
  11. services.PostConfigureAll<DateTimeOptions>(options =>
  12. {
  13. Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置");
  14. });
  15. }

Options 体系

IConfigureOptions

该接口用于包装对选项的配置。默认实现为ConfigureOptions

  1. public interface IConfigureOptions<in TOptions> where TOptions : class
  2. {
  3. void Configure(TOptions options);
  4. }
ConfigureOptions
  1. public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class
  2. {
  3. public ConfigureOptions(Action<TOptions> action)
  4. {
  5. Action = action;
  6. }
  7. public Action<TOptions> Action { get; }
  8. // 配置 TOptions 实例
  9. public virtual void Configure(TOptions options)
  10. {
  11. Action?.Invoke(options);
  12. }
  13. }
ConfigureFromConfigurationOptions

该类通过继承类ConfigureOptions,对选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到选项上:

  1. public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>
  2. where TOptions : class
  3. {
  4. public ConfigureFromConfigurationOptions(IConfiguration config)
  5. : base(options => ConfigurationBinder.Bind(config, options))
  6. { }
  7. }

IConfigureNamedOptions

该接口用于包装对命名选项的配置,该接口同时继承了接口IConfigureOptions的行为,默认实现为ConfigureNamedOptions,另外为了实现“使用 DI 服务配置选项”的功能,还提供了一些泛型类重载。

  1. public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
  2. {
  3. void Configure(string name, TOptions options);
  4. }
ConfigureNamedOptions
  1. public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
  2. {
  3. public ConfigureNamedOptions(string name, Action<TOptions> action)
  4. {
  5. Name = name;
  6. Action = action;
  7. }
  8. public string Name { get; }
  9. public Action<TOptions> Action { get; }
  10. public virtual void Configure(string name, TOptions options)
  11. {
  12. // Name == null 表示针对 TOptions 的所有实例进行配置
  13. if (Name == null || name == Name)
  14. {
  15. Action?.Invoke(options);
  16. }
  17. }
  18. public void Configure(TOptions options) => Configure(Options.DefaultName, options);
  19. }
NamedConfigureFromConfigurationOptions

该类通过继承类ConfigureNamedOptions,对命名选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到命名选项上:

  1. public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
  2. where TOptions : class
  3. {
  4. public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
  5. : this(name, config, _ => { })
  6. { }
  7. public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
  8. : base(name, options => config.Bind(options, configureBinder))
  9. { }
  10. }

IPostConfigureOptions

该接口用于包装对命名选项的后期配置,将在所有IConfigureOptions执行完毕后才会执行,默认实现为PostConfigureOptions,同样的,为了实现“使用 DI 服务对选项进行后期配置”的功能,也提供了一些泛型类重载:

  1. public interface IPostConfigureOptions<in TOptions> where TOptions : class
  2. {
  3. void PostConfigure(string name, TOptions options);
  4. }
  5. public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
  6. {
  7. public PostConfigureOptions(string name, Action<TOptions> action)
  8. {
  9. Name = name;
  10. Action = action;
  11. }
  12. public string Name { get; }
  13. public Action<TOptions> Action { get; }
  14. public virtual void PostConfigure(string name, TOptions options)
  15. {
  16. // Name == null 表示针对 TOptions 的所有实例进行后期配置
  17. if (Name == null || name == Name)
  18. {
  19. Action?.Invoke(options);
  20. }
  21. }
  22. }

AddOptions & AddOptions & OptionsBuilder

  1. public static class OptionsServiceCollectionExtensions
  2. {
  3. // 该方法帮我们把一些常用的与 Options 相关的服务注入到 DI 容器
  4. public static IServiceCollection AddOptions(this IServiceCollection services)
  5. {
  6. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
  7. services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
  8. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
  9. services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
  10. services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
  11. return services;
  12. }
  13. // 没有指定 Options 名称时,默认使用 Options.DefaultName
  14. public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class
  15. => services.AddOptions<TOptions>(Options.Options.DefaultName);
  16. // 由于后续还要对 TOptions 进行配置,所以返回一个 OptionsBuilder 出去
  17. public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
  18. where TOptions : class
  19. {
  20. services.AddOptions();
  21. return new OptionsBuilder<TOptions>(services, name);
  22. }
  23. }

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

  1. public class OptionsBuilder<TOptions> where TOptions : class
  2. {
  3. private const string DefaultValidationFailureMessage = "A validation error has occurred.";
  4. // TOptions 实例的名字
  5. public string Name { get; }
  6. public IServiceCollection Services { get; }
  7. public OptionsBuilder(IServiceCollection services, string name)
  8. {
  9. Services = services;
  10. Name = name ?? Options.DefaultName;
  11. }
  12. // 选项配置
  13. public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions)
  14. {
  15. Services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(Name, configureOptions));
  16. return this;
  17. }
  18. // 选项后期配置
  19. public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions)
  20. {
  21. Services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(Name, configureOptions));
  22. return this;
  23. }
  24. // 选项验证
  25. public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation)
  26. => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage);
  27. public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage)
  28. {
  29. Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage));
  30. return this;
  31. }
  32. }

OptionsServiceCollectionExtensions.Configure

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

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

OptionsConfigurationServiceCollectionExtensions.Configure

请注意,该Configure方法与上方提及的Configure不是同一个。该扩展方法针对配置(IConfiguration)绑定到选项(Options)上进行了扩展

Install-Package Microsoft.Extensions.Options.ConfigurationExtensions

  1. public static class OptionsConfigurationServiceCollectionExtensions
  2. {
  3. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
  4. => services.Configure<TOptions>(Options.Options.DefaultName, config);
  5. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
  6. => services.Configure<TOptions>(name, config, _ => { });
  7. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
  8. where TOptions : class
  9. => services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);
  10. public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
  11. where TOptions : class
  12. {
  13. services.AddOptions();
  14. services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
  15. return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
  16. }
  17. }

IOptionsFactory

IOptionsFactory负责创建命名选项实例,默认实现为OptionsFactory

  1. public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class
  2. {
  3. TOptions Create(string name);
  4. }
  5. public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions>
  6. : IOptionsFactory<TOptions> where TOptions : class
  7. {
  8. private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
  9. private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
  10. private readonly IEnumerable<IValidateOptions<TOptions>> _validations;
  11. // 这里通过依赖注入的的方式将与 TOptions 相关的配置、验证服务列表解析出来
  12. public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
  13. : this(setups, postConfigures, validations: null)
  14. { }
  15. public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
  16. {
  17. _setups = setups;
  18. _postConfigures = postConfigures;
  19. _validations = validations;
  20. }
  21. public TOptions Create(string name)
  22. {
  23. // 1. 创建并配置 Options
  24. TOptions options = CreateInstance(name);
  25. foreach (IConfigureOptions<TOptions> setup in _setups)
  26. {
  27. if (setup is IConfigureNamedOptions<TOptions> namedSetup)
  28. {
  29. namedSetup.Configure(name, options);
  30. }
  31. else if (name == Options.DefaultName)
  32. {
  33. setup.Configure(options);
  34. }
  35. }
  36. // 2. 对 Options 进行后期配置
  37. foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
  38. {
  39. post.PostConfigure(name, options);
  40. }
  41. // 3. 执行 Options 校验
  42. if (_validations != null)
  43. {
  44. var failures = new List<string>();
  45. foreach (IValidateOptions<TOptions> validate in _validations)
  46. {
  47. ValidateOptionsResult result = validate.Validate(name, options);
  48. if (result.Failed)
  49. {
  50. failures.AddRange(result.Failures);
  51. }
  52. }
  53. if (failures.Count > 0)
  54. {
  55. throw new OptionsValidationException(name, typeof(TOptions), failures);
  56. }
  57. }
  58. return options;
  59. }
  60. protected virtual TOptions CreateInstance(string name)
  61. {
  62. return Activator.CreateInstance<TOptions>();
  63. }
  64. }

OptionsManager

通过AddOptions扩展方法的实现,可以看到,IOptions和IOptionsSnapshot的实现都是OptionsManager,只不过一个是 Singleton,一个是 Scoped。我们通过前面的分析也知道了,当源中的配置改变时,IOptions始终维持初始值,IOptionsSnapshot在每次请求时会读取最新配置值,并在同一个请求中是不变的。接下来就来看看OptionsManager是如何实现的:

  1. public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
  2. IOptions<TOptions>,
  3. IOptionsSnapshot<TOptions>
  4. where TOptions : class
  5. {
  6. private readonly IOptionsFactory<TOptions> _factory;
  7. // 将已创建的 TOptions 实例缓存到该私有变量中
  8. private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>();
  9. public OptionsManager(IOptionsFactory<TOptions> factory)
  10. {
  11. _factory = factory;
  12. }
  13. public TOptions Value => Get(Options.DefaultName);
  14. public virtual TOptions Get(string name)
  15. {
  16. name = name ?? Options.DefaultName;
  17. // 若缓存不存在,则通过工厂新建 Options 实例,否则直接读取缓存
  18. return _cache.GetOrAdd(name, () => _factory.Create(name));
  19. }
  20. }

OptionsMonitor

同样,通过前面的分析,我们知道OptionsMonitor读取的始终是配置的最新值,它的实现在OptionsManager的基础上,除了使用缓存将创建的 Options 实例缓存起来外,还增添了监听机制,当配置发生更改时,会将缓存移除。

  1. public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
  2. IOptionsMonitor<TOptions>,
  3. IDisposable
  4. where TOptions : class
  5. {
  6. private readonly IOptionsMonitorCache<TOptions> _cache;
  7. private readonly IOptionsFactory<TOptions> _factory;
  8. private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
  9. private readonly List<IDisposable> _registrations = new List<IDisposable>();
  10. internal event Action<TOptions, string> _onChange;
  11. public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
  12. {
  13. _factory = factory;
  14. _sources = sources;
  15. _cache = cache;
  16. // 监听更改
  17. foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
  18. {
  19. IDisposable registration = ChangeToken.OnChange(
  20. () => source.GetChangeToken(),
  21. (name) => InvokeChanged(name),
  22. source.Name);
  23. _registrations.Add(registration);
  24. }
  25. }
  26. // 当发生更改时,移除缓存
  27. private void InvokeChanged(string name)
  28. {
  29. name = name ?? Options.DefaultName;
  30. _cache.TryRemove(name);
  31. TOptions options = Get(name);
  32. if (_onChange != null)
  33. {
  34. _onChange.Invoke(options, name);
  35. }
  36. }
  37. public TOptions CurrentValue => Get(Options.DefaultName);
  38. public virtual TOptions Get(string name)
  39. {
  40. name = name ?? Options.DefaultName;
  41. return _cache.GetOrAdd(name, () => _factory.Create(name));
  42. }
  43. // 通过该方法绑定 OnChange 事件
  44. public IDisposable OnChange(Action<TOptions, string> listener)
  45. {
  46. var disposable = new ChangeTrackerDisposable(this, listener);
  47. _onChange += disposable.OnChange;
  48. return disposable;
  49. }
  50. public void Dispose()
  51. {
  52. // 移除所有 change token 的订阅
  53. foreach (IDisposable registration in _registrations)
  54. {
  55. registration.Dispose();
  56. }
  57. _registrations.Clear();
  58. }
  59. }

总结

  • 所有选项均为命名选项,默认名称为Options.DefaultName,即string.Empty。
  • 通过ConfigurationBinder.Get或ConfigurationBinder.Bind手动获取选项实例。
  • 通过Configure方法进行选项配置:
    • OptionsBuilder.Configure:通过包含DI服务的委托来进行选项配置
    • OptionsServiceCollectionExtensions.Configure:通过简单委托来进行选项配置
    • OptionsConfigurationServiceCollectionExtensions.Configure:直接将IConfiguration实例绑定到选项上
  • 通过OptionsServiceCollectionExtensions.ConfigureAll方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过PostConfigure方法进行选项后期配置:
    • OptionsBuilder.PostConfigure:通过包含DI服务的委托来进行选项后期配置
    • OptionsServiceCollectionExtensions.PostConfigure:通过简单委托来进行选项后期配置
  • 通过PostConfigureAll方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过Validate进行选项验证:
    • OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations:通过数据注解进行选项验证
    • OptionsBuilder.Validate:通过委托进行选项验证
    • IValidateOptions:通过实现该接口并注入实现来进行选项验证
  • 通过依赖注入读取选项:
    • IOptions:Singleton,值永远是该接口被实例化时的选项配置初始值
    • IOptionsSnapshot:Scoped,每一次Http请求开始时会读取选项配置的最新值,并在当前请求中保持不变
    • IOptionsMonitor:Singleton,每次读取都是选项配置的最新值
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么... 返回顶部
About 京ICP备13038605号 © 代码片段 2025