注:本文隶属于《理解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; }
}

最后进行绑定(有BindGet两种方式):

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>();
}
}

其中,属性IdTitleAuthor均会与配置进行绑定,但是字段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中的服务,这时可以借助OptionsBuilderConfigure方法(注意这个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 false if (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后期配置

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

  • 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行
  • ConfigureConfigureAll类似,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.DefaultName
public 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.DefaultName
public 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. 创建并配置 Options
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);
}
} // 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>,
IDisposable
where 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.GetConfigurationBinder.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 - 选项(Options)的更多相关文章

  1. asp.net core选项Options模块的笔记

    这篇博客是写给自己看的.已经不止一次看到AddOptions的出现,不管是在.net core源码还是别人的框架里面,都充斥着AddOptions.于是自己大概研究了下,没有深入,因为,我的功力还是不 ...

  2. 理解ASP.NET Core - 配置(Configuration)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...

  3. 理解ASP.NET Core - 文件服务器(File Server)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 提供静态文件 静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(C ...

  4. 理解ASP.NET Core - 基于Cookie的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 概述 通常,身份认证(Authentication)和授权(Authorization)都会放 ...

  5. 理解ASP.NET Core - 基于JwtBearer的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 在开始之前,如果你还不了解基于Cookie的身份认证,那么建议你先阅读<基于Cookie ...

  6. 理解ASP.NET Core - 授权(Authorization)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 之前,我们已经了解了ASP.NET Core中的身份认证,现在,我们来聊一下授权. 老规矩,示 ...

  7. 理解ASP.NET Core - 发送Http请求(HttpClient)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 前言 在.NET中,我们有很多发送Http请求的手段,如HttpWebRequest.WebC ...

  8. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  9. 目录-理解ASP.NET Core

    <理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...

随机推荐

  1. reids rdb与aof

    rdb:时合高并发场景,容易备份恢复,会丢失部分数据 1.默认开启的方式,可以进过压缩,可以根据时间点生成快照 2.数据量大的情况下恢复快 3.bgsave一边开启fork保存文件,一边继续响应客户端 ...

  2. spring支持的Bean的作用域

    Sigleton:单例模式,在整个Spring IoC容器中,使用Sigleton定义Bean将有一个实例 prototype:原型模式,每次通过容器的getBean方法获取propertype都将产 ...

  3. PyQT5基础布局管理

    绝对定位布局 使用move(x, y)可以对窗口进行布局,以窗口左上角为原点,向右为 x 轴正方向,向下为 y 轴正方向,移动(x,y); import sys from PyQt5.QtGui im ...

  4. Mysql - You can't specify target table '表名' for update in FROM clause 错误解决办法

    背景 在MySQL中,写SQL语句的时候 ,可能会遇到 You can't specify target table '表名' for update in FROM clause 这样的错误 错误含义 ...

  5. Docker(34)- 如何修改 docker 容器的目录映射

    如果你还想从头学起 Docker,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1870863.html 问题背景 docker run ...

  6. Spring MVC拦截器浅析

    Spring MVC拦截器 重点:Spring MVC的拦截器只会拦截控制器的请求,如果是jsp.js.image.html则会放行. 什么是拦截器 运行在服务器的程序,先于Servlet或JSP之前 ...

  7. Intel® QAT加速卡之加密、哈希操作流程和示例

    Intel QAT 加密API介绍 文章主要讲述了Intel QAT 加密API接口的说明,以及多种应用场景下的使用方法. 文章目录 Intel QAT 加密API介绍 1. 概述 1.1 会话(se ...

  8. SQL-UPDATE触发器练习

    &练习一 如下所示三张表( student,grade,student_updata_before ): student表 grade表 Student_update_before表 # 触发 ...

  9. bean的作用域和生命周期

    一.Bean作用域 二.生命周期 其中,这个类实现各种接口重写各种方法,会按bean的声明周期按序执行: 其中,自定义的初始化和自定义销毁的方法不是实现接口重写,而是成员方法,并且在装配bean即在x ...

  10. 三剑客之sed编辑器 基操

    目录: 一.sed编辑器 二.打印内容 三.使用地址 四.删除行 五.替换 六.插入 一.sed编辑器 sed是一种流编辑器,流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流. sed ...