CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
理解ASP.NET Core - 选项(Options)
0
.NetCore
小笨蛋
发布于:2022年09月26日
更新于:2022年09月26日
108
#custom-toc-container
### Options绑定 中文译为“选项”,该功能用于实现以强类型的方式对程序配置信息进行访问。 既然是强类型的方式,那么就需要定义一个Options类,该类: - 推荐命名规则:{Object}Options - 特点: - 非抽象类 - 必须包含公共无参的构造函数 - 类中的所有公共读写属性都会与配置项进行绑定 - 字段不会被绑定 接下来,为了便于理解,先举个例子: 首先在 appsetting.json 中添加如下配置: ```csharp { "Book": { "Id": 1, "Name": "三国演义", "Author": "罗贯中" } } ``` 然后定义Options类: ```csharp 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两种方式): ```csharp 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
(); } } ``` 其中,属性Id、Title、Author均会与配置进行绑定,但是字段Book并不会被绑定,该字段只是用来让我们避免在程序中使用“魔数”。另外,一定要确保配置项能够转换到其绑定的属性类型(你该不会想把string绑定到int类型上吧)。 当然,这样写代码还不够完美,还是要将Options添加到依赖注入服务容器中,例如通过IServiceCollection的扩展方法Configure: ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.Configure
(Configuration.GetSection(BookOptions.Book)); } } ``` ### Options读取 通过Options接口,我们可以读取依赖注入容器中的Options。常用的有三个接口: - `IOptions
` - `IOptionsSnapshot
` - `IOptionsMonitor
` 接下来,我们看看它们的区别。 #### IOptions - 该接口对象实例生命周期为 Singleton,因此能够将该接口注入到任何生命周期的服务中 - 当该接口被实例化后,其中的选项值将永远保持不变,即使后续修改了与选项进行绑定的配置,也永远读取不到修改后的配置值 - 不支持命名选项(Named Options),这个下面会说 ```csharp public class ValuesController : ControllerBase { private readonly BookOptions _bookOptions; public ValuesController(IOptions
bookOptions) { // bookOptions.Value 始终是程序启动时加载的配置,永远不会改变 _bookOptions = bookOptions.Value; } } ``` #### IOptionsSnapshot - 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。 - 在作用域中,创建IOptionsSnapshot
对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。 - 支持命名选项 ```csharp public class ValuesController : ControllerBase { private readonly BookOptions _bookOptions; public ValuesController(IOptionsSnapshot
bookOptionsSnapshot) { // bookOptions.Value 是 Options 对象实例创建时读取的配置快照 _bookOptions = bookOptionsSnapshot.Value; } } ``` #### IOptionsMonitor - 该接口除了可以查看TOptions的值,还可以监控TOptions配置的更改。 - 该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中 - 每次读取选项值时,都是从配置中读取最新选项值(具体读取逻辑查看下方三种接口对比测试)。 - 支持: - 命名选项 - 重新加载配置(CurrentValue),并当配置发生更改时,进行通知(OnChange) - 缓存与缓存失效 (IOptionsMonitorCache
) ```csharp public class ValuesController : ControllerBase { private readonly IOptionsMonitor
_bookOptionsMonitor; public ValuesController(IOptionsMonitor
bookOptionsMonitor) { // _bookOptionsMonitor.CurrentValue 的值始终是最新配置的值 _bookOptionsMonitor = bookOptionsMonitor; } } ``` #### 三种接口对比测试 IOptions
就不说了,主要说一下IOptionsSnapshot
和IOptionsMonitor
的不同: - IOptionsSnapshot
注册为 Scoped,在创建其实例时,会从配置中读取最新选项值作为快照,并在作用域中使用该快照 - IOptionsMonitor
注册为 Singleton,每次调用实例的 CurrentValue 时,会先检查缓存(IOptionsMonitorCache
)是否有值,如果有值,则直接用,如果没有,则从配置中读取最新选项值,并记入缓存。当配置发生更改时,会将缓存清空。 搞个测试小程序: ```csharp [ApiController] [Route("[controller]")] public class ValuesController : ControllerBase { private readonly IOptions
_bookOptions; private readonly IOptionsSnapshot
_bookOptionsSnapshot; private readonly IOptionsMonitor
_bookOptionsMonitor; public ValuesController( IOptions
bookOptions, IOptionsSnapshot
bookOptionsSnapshot, IOptionsMonitor
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次输出: ```csharp { "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次输出: ```csharp { // 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 中添加如下配置 ```csharp { "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类,只需要创建一个就好了: ```csharp 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容器中: ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.Configure
(Configuration.GetSection(BookOptions.Book)); services.Configure
(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}")); services.Configure
(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}")); } } ``` 最后,通过构造函数的方式将选项注入到Controller中。需要注意的是,因为DateTimeOptions类绑定了两个选项类,所以当我们获取时选项值时,需要指定选项的名字。 ```csharp public class ValuesController : ControllerBase { private readonly DateTimeOptions _beijingDateTimeOptions; private readonly DateTimeOptions _tockyoDateTimeOptions; public ValuesController(IOptionsSnapshot
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](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.options.optionsbuilder-1?view=dotnet-plat-ext-5.0 "OptionsBuilder")的Configure方法(注意这个Configure不是上面提到的IServiceCollection的扩展方法Configure,这是两个不同的方法),该方法支持最多5个服务来配置选项: ```csharp services.AddOptions
() .Configure
((o, s, s2, s3, s4, s5) => { o.Authors = DoSomethingWith(s, s2, s3, s4, s5); }); ``` ### Options 验证 配置毕竟是我们手动进行文本输入的,难免会出现错误,这种情况下,就需要使用程序来帮助进行校验了。 #### DataAnnotations > Install-Package Microsoft.Extensions.Options.DataAnnotations 我们先升级一下BookOptions,增加一些数据校验: ```csharp 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容器时,增加数据注解验证: ```csharp public void ConfigureServices(IServiceCollection services) { services.AddOptions
() .Bind(Configuration.GetSection(BookOptions.Book)) .ValidateDataAnnotations(); .Validate(options => { // 校验通过 return true // 校验失败 return false if (options.Author.Contains("A")) { return false; } return true; }); } ``` ValidateDataAnnotations会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用Validate进行较为复杂的校验,如果过于复杂,则就要用到IValidateOptions了(实质上,Validate方法内部也是通过注入一个IValidateOptions实例来实现选项验证的)。 #### IValidateOptions 通过实现IValidateOptions
接口,增加数据校验规则,例如: ```csharp public class BookValidation : IValidateOptions
{ public ValidateOptionsResult Validate(string name, BookOptions options) { var failures = new List
(); 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实例都会被执行: ```csharp public void ConfigureServices(IServiceCollection services) { services.Configure
(Configuration.GetSection(BookOptions.Book)); services.TryAddEnumerable(ServiceDescriptor.Singleton
, BookValidation>()); } ``` ### Options后期配置 介绍两个方法,分别是PostConfigure和PostConfigureAll,他们用来对选项进行后期配置。 - 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行 - 与Configure和ConfigureAll类似,PostConfigure仅用于对指定名称的选项进行后期配置(默认名称为string.Empty),PostConfigureAll则用于对所有选项实例进行后期配置 - 每当选项更改时,均会触发相应的方法 ```csharp public void ConfigureServices(IServiceCollection services) { services.PostConfigure
(options => { Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置"); }); services.PostConfigure
(DateTimeOptions.Beijing, options => { Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置"); }); services.PostConfigureAll
(options => { Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置"); }); } ``` ### Options 体系 #### IConfigureOptions 该接口用于包装对选项的配置。默认实现为ConfigureOptions
。 ```csharp public interface IConfigureOptions
where TOptions : class { void Configure(TOptions options); } ``` ##### ConfigureOptions ```csharp public class ConfigureOptions
: IConfigureOptions
where TOptions : class { public ConfigureOptions(Action
action) { Action = action; } public Action
Action { get; } // 配置 TOptions 实例 public virtual void Configure(TOptions options) { Action?.Invoke(options); } } ``` ##### ConfigureFromConfigurationOptions 该类通过继承类ConfigureOptions
,对选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到选项上: ```csharp public class ConfigureFromConfigurationOptions
: ConfigureOptions
where TOptions : class { public ConfigureFromConfigurationOptions(IConfiguration config) : base(options => ConfigurationBinder.Bind(config, options)) { } } ``` #### IConfigureNamedOptions 该接口用于包装对命名选项的配置,该接口同时继承了接口IConfigureOptions
的行为,默认实现为ConfigureNamedOptions
,另外为了实现“使用 DI 服务配置选项”的功能,还提供了一些泛型类重载。 ```csharp public interface IConfigureNamedOptions
: IConfigureOptions
where TOptions : class { void Configure(string name, TOptions options); } ``` ##### ConfigureNamedOptions ```csharp public class ConfigureNamedOptions
: IConfigureNamedOptions
where TOptions : class { public ConfigureNamedOptions(string name, Action
action) { Name = name; Action = action; } public string Name { get; } public Action
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
,对命名选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到命名选项上: ```csharp public class NamedConfigureFromConfigurationOptions
: ConfigureNamedOptions
where TOptions : class { public NamedConfigureFromConfigurationOptions(string name, IConfiguration config) : this(name, config, _ => { }) { } public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action
configureBinder) : base(name, options => config.Bind(options, configureBinder)) { } } ``` #### IPostConfigureOptions 该接口用于包装对命名选项的后期配置,将在所有IConfigureOptions
执行完毕后才会执行,默认实现为PostConfigureOptions
,同样的,为了实现“使用 DI 服务对选项进行后期配置”的功能,也提供了一些泛型类重载: ```csharp public interface IPostConfigureOptions
where TOptions : class { void PostConfigure(string name, TOptions options); } public class PostConfigureOptions
: IPostConfigureOptions
where TOptions : class { public PostConfigureOptions(string name, Action
action) { Name = name; Action = action; } public string Name { get; } public Action
Action { get; } public virtual void PostConfigure(string name, TOptions options) { // Name == null 表示针对 TOptions 的所有实例进行后期配置 if (Name == null || name == Name) { Action?.Invoke(options); } } } ``` #### AddOptions & AddOptions & OptionsBuilder ```csharp 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.DefaultName public static OptionsBuilder
AddOptions
(this IServiceCollection services) where TOptions : class => services.AddOptions
(Options.Options.DefaultName); // 由于后续还要对 TOptions 进行配置,所以返回一个 OptionsBuilder 出去 public static OptionsBuilder
AddOptions
(this IServiceCollection services, string name) where TOptions : class { services.AddOptions(); return new OptionsBuilder
(services, name); } } ``` 那我们看看OptionsBuilder
可以配置哪些东西,由于该类中有大量重载方法,我只挑选最基础的方法来看一看: ```csharp public class OptionsBuilder
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
Configure(Action
configureOptions) { Services.AddSingleton
>(new ConfigureNamedOptions
(Name, configureOptions)); return this; } // 选项后期配置 public virtual OptionsBuilder
PostConfigure(Action
configureOptions) { Services.AddSingleton
>(new PostConfigureOptions
(Name, configureOptions)); return this; } // 选项验证 public virtual OptionsBuilder
Validate(Func
validation) => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); public virtual OptionsBuilder
Validate(Func
validation, string failureMessage) { Services.AddSingleton
>(new ValidateOptions
(Name, validation, failureMessage)); return this; } } ``` #### OptionsServiceCollectionExtensions.Configure OptionsServiceCollectionExtensions.Configure
实际上就是对选项的一般配置方式进行了封装,免去了OptionsBuilder
: ```csharp public static class OptionsServiceCollectionExtensions { // 没有指定 Options 名称时,默认使用 Options.DefaultName public static IServiceCollection Configure
(this IServiceCollection services, Action
configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions); // 等同于做了 AddOptions
和 OptionsBuilder
.Configure 两件事 public static IServiceCollection Configure
(this IServiceCollection services, string name, Action
configureOptions) where TOptions : class { services.AddOptions(); services.AddSingleton
>(new ConfigureNamedOptions
(name, configureOptions)); return services; } // 由于 ConfigureAll 是针对 TOptions 的所有实例进行配置,所以不需要指定名字 public static IServiceCollection ConfigureAll
(this IServiceCollection services, Action
configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions); } ``` #### OptionsConfigurationServiceCollectionExtensions.Configure 请注意,该Configure
方法与上方提及的Configure
不是同一个。该扩展方法针对配置(IConfiguration)绑定到选项(Options)上进行了扩展 > Install-Package Microsoft.Extensions.Options.ConfigurationExtensions ```csharp public static class OptionsConfigurationServiceCollectionExtensions { public static IServiceCollection Configure
(this IServiceCollection services, IConfiguration config) where TOptions : class => services.Configure
(Options.Options.DefaultName, config); public static IServiceCollection Configure
(this IServiceCollection services, string name, IConfiguration config) where TOptions : class => services.Configure
(name, config, _ => { }); public static IServiceCollection Configure
(this IServiceCollection services, IConfiguration config, Action
configureBinder) where TOptions : class => services.Configure
(Options.Options.DefaultName, config, configureBinder); public static IServiceCollection Configure
(this IServiceCollection services, string name, IConfiguration config, Action
configureBinder) where TOptions : class { services.AddOptions(); services.AddSingleton
>(new ConfigurationChangeTokenSource
(name, config)); return services.AddSingleton
>(new NamedConfigureFromConfigurationOptions
(name, config, configureBinder)); } } ``` #### IOptionsFactory IOptionsFactory
负责创建命名选项实例,默认实现为OptionsFactory
: ```csharp public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class { TOptions Create(string name); } public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsFactory
where TOptions : class { private readonly IEnumerable
> _setups; private readonly IEnumerable
> _postConfigures; private readonly IEnumerable
> _validations; // 这里通过依赖注入的的方式将与 TOptions 相关的配置、验证服务列表解析出来 public OptionsFactory(IEnumerable
> setups, IEnumerable
> postConfigures) : this(setups, postConfigures, validations: null) { } public OptionsFactory(IEnumerable
> setups, IEnumerable
> postConfigures, IEnumerable
> validations) { _setups = setups; _postConfigures = postConfigures; _validations = validations; } public TOptions Create(string name) { // 1. 创建并配置 Options TOptions options = CreateInstance(name); foreach (IConfigureOptions
setup in _setups) { if (setup is IConfigureNamedOptions
namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } // 2. 对 Options 进行后期配置 foreach (IPostConfigureOptions
post in _postConfigures) { post.PostConfigure(name, options); } // 3. 执行 Options 校验 if (_validations != null) { var failures = new List
(); foreach (IValidateOptions
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
(); } } ``` #### OptionsManager 通过AddOptions扩展方法的实现,可以看到,IOptions
和IOptionsSnapshot
的实现都是OptionsManager
,只不过一个是 Singleton,一个是 Scoped。我们通过前面的分析也知道了,当源中的配置改变时,IOptions
始终维持初始值,IOptionsSnapshot
在每次请求时会读取最新配置值,并在同一个请求中是不变的。接下来就来看看OptionsManager
是如何实现的: ```csharp public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptions
, IOptionsSnapshot
where TOptions : class { private readonly IOptionsFactory
_factory; // 将已创建的 TOptions 实例缓存到该私有变量中 private readonly OptionsCache
_cache = new OptionsCache
(); public OptionsManager(IOptionsFactory
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
读取的始终是配置的最新值,它的实现在OptionsManager
的基础上,除了使用缓存将创建的 Options 实例缓存起来外,还增添了监听机制,当配置发生更改时,会将缓存移除。 ```csharp public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsMonitor
, IDisposable where TOptions : class { private readonly IOptionsMonitorCache
_cache; private readonly IOptionsFactory
_factory; private readonly IEnumerable
> _sources; private readonly List
_registrations = new List
(); internal event Action
_onChange; public OptionsMonitor(IOptionsFactory
factory, IEnumerable
> sources, IOptionsMonitorCache
cache) { _factory = factory; _sources = sources; _cache = cache; // 监听更改 foreach (IOptionsChangeTokenSource
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
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
.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号
© 代码片段 2024