翻译 - ASP.NET Core 基本知识 - 选项(Options)
翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0
ASP.NET Core 中的选项模式
选项模式使用类为访问一组相关的设置提供强类型支持。当配置设置(configuration settings)根据情景被隔离到不同的类中时,应用程序应该遵守以下两个重要的软件工程原则:
- 接口分离原则或者封装( Interface Segregation Principle (ISP) or Encapsulation)原则:依赖于配置设置的情景(类)只依赖于它们使用的配置设置
- 分离关注点:应用程序不同部分的设置不应该依赖或者和另一个耦合在一起
选项也提供了一种验证配置数据的机制。更多信息查看 Options validation 。
该主题提供了 ASP.NET Core 中的选项模式的有关信息。有关在控制台应用程序中使用选项模式的信息,查看 Options pattern in .NET。
查看或下载示例程序(View or download sample code (how to download))。
绑定多级配置
读取配置值的优先方式是使用选项模式(options pattern)。例如,读取下列配置值:
"Position": { "Title": "Editor", "Name": "Joe Smith" }
创建下面的 PositionOptions 类:
public class PositionOptions { public const string Position = "Position"; public string Title { get; set; } public string Name { get; set; } }
选项类:
- 必须是带有公开无惨构造方法的非抽象类
- 所有公开的可读写属性的类型都被绑定
- 字段不会绑定。在上面的代码中,Position 不会绑定。使用 Position 属性的好处是在应用程序中绑定类到配置提供器时,字符串 "Position" 不用在程序中硬编码
以下代码:
- 调用 ConfigurationBinder.Bind 绑定 PositionOptions 类到 Position 区域
- 输出展示 Position 配置数据
public class Test22Model : PageModel { private readonly IConfiguration Configuration; public Test22Model(IConfiguration configuration) { Configuration = configuration; } public ContentResult OnGet() { var positionOptions = new PositionOptions(); Configuration.GetSection(positionOptions.Position).Bind(positionOptions); return Content($"Title: {positionOptions.Title} \n" + $"Name: {positionOptions.Name}"); } }
默认的,以上代码在应用程序启动 JSON 配置文件更改后会被读取。
ConfigurationBinder.Get<T> 绑定和返回指定类型。ConfigurationBinder.Get<T> 可能比使用 ConfigurationBinder.Bind 更加方便。下面的代码展示了如果 PositionOptions 类使用 ConfigurationBinder.Get<T> 方法:
public class Test21Model : PageModel { private readonly IConfiguration Configuration; public PositionOptions positionOptions { get; private set; } public Test21Model(IConfiguration configuration) { Configuration = configuration; } public ContentResult OnGet() { positionOptions = Configuration.GetSection(positionOptions.Position) .Get<PositionOptions>(); return Content($"Title: {positionOptions.Title} \n" + $"Name: {positionOptions.Name}"); } }
默认的,以上代码在应用程序启动后 JSON 配置文件的改变也会被读取。
一种使用选项模式(options pattern)的方式是绑定 Position 区域,并且把它添加到依赖注入服务容器中(dependency injection service container)。下面的代码中, PositionOptions 使用 Confiure 添加到服务容器中,然后绑定到配置:
public void ConfigureServices(IServiceCollection services) { services.Configure<PositionOptions>(Configuration.GetSection( PositionOptions.Position)); services.AddRazorPages(); }
使用上面的代码,下面的代码读取 position 选项:
public class Test2Model : PageModel { private readonly PositionOptions _options; public Test2Model(IOptions<PositionOptions> options) { _options = options.Value; } public ContentResult OnGet() { return Content($"Title: {_options.Title} \n" + $"Name: {_options.Name}"); } }
上面的代码中,应用程序启动后,对 JSON 配置文件的更改不会被读取。如果要读取应用程序启动后的配置修改,可以使用 IOptionsSnapshot。
选项接口
- 不支持:
在应用程序启动后读取配置数据
命名选项(Named options) - 作为单例(Singleton)注册,可以在任何服务生命周期(service lifetime)被注入
- 选项在每一次请求都需要重新计算的场景中很有用。更多信息,查看 Use IOptionsSnapshot to read updated data
- 作为有范围的(Scoped)的服务注册,因此,不能够被注入到一个单例服务
- 支持命名选项(named options)
- 用来获取选项和管理 TOptions 实例的选项通知
- 作为单例(Singleton),可以注入到任意服务生命周期(service lifetime)
- 支持:
更改通知
命名选项(Named options)
重新加载配置(Reloadable configuration)
选择性选项无效(IOptionsMonitorCache<TOptions>)
在所有 IConfigureOptions<TOptions> 发生后,Post-configuration 场景使能设置或者改变。
IOptionsFactory<TOptions> 负责创建一个新的选项实例。它有一个 Create 方法。默认的实现带有所有注册的 IConfigureOptions<TOptions> 和 IPostConfigureOptions<TOptions>,并且会首先运行所有配置,之后是 post-configuration。它不同于 IConfigureNamedOptions<TOptions> 和 IConfigureOptions<TOptions>,并且仅仅调用适合的接口。
IOptionsMonitorCache<TOptions> 被 IOptionsMonitor<TOptions> 使用来缓存 TOptions 实例。IOptionsMonitorCache<TOptions> 在监视器中验证选项实例,所以值会被重新计算(TryRemove)。值可以使用 TryAdd 手动添加。Clear 方法在所有命名实例在需要的时候应该被创建的时候被使用。
使用 IOptionsSnapshot 读取更新后的数据
使用接口 IOptionsSnapshot<TOptions> 的时候,选项在每一次请求被访问的时候都会重新计算,并且在请求的生命周期内都会缓存。在应用程序启动后,当使用支持读取更新后配置值的配置提供器的时候,对于配置的更改就会被读取。
接口 IOptionsMonitor 和 IOptionsSnapshot 的不同是:
- IOptionsMonitor 是一个单例服务 (singleton service) ,在任何时间都可以获取当前选项值,在单例依赖中特别有用
- IOptionsSnapshot 是一个有作用域的服务(scoped service),在 IOptionsSnapshot<T> 对象被创建的时候提供一个选项的快照。选项快照是为使用短暂和有作用域的依赖设计的
下面的代码使用 IOptionsSnapshot<TOptions>:
public class TestSnapModel : PageModel { private readonly MyOptions _snapshotOptions; public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor) { _snapshotOptions = snapshotOptionsAccessor.Value; } public ContentResult OnGet() { return Content($"Option1: {_snapshotOptions.Option1} \n" + $"Option2: {_snapshotOptions.Option2}"); } }
下面的代码注册了一个 MyOptions 绑定到的配置实例:
public void ConfigureServices(IServiceCollection services) { services.Configure<MyOptions>(Configuration.GetSection("MyOptions")); services.AddRazorPages(); }
上面的代码,应用程序启动后对于 JSON 配置文件的改变也会被读取。
IOptionsMonitor
下面的代码注册了一个 MyOptions 绑定到的配置实例:
public void ConfigureServices(IServiceCollection services) { services.Configure<MyOptions>(Configuration.GetSection("MyOptions")); services.AddRazorPages(); }
下面的例子使用了 IOptionsMonitor<TOptions>:
public class TestMonitorModel : PageModel { private readonly IOptionsMonitor<MyOptions> _optionsDelegate; public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate ) { _optionsDelegate = optionsDelegate; } public ContentResult OnGet() { return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" + $"Option2: {_optionsDelegate.CurrentValue.Option2}"); } }
上面的代码,应用程序启动后对于 JSON 配置文件的改变也会被读取。
使用 IConfigureNamedOptions 支持命名选项
命名选项:
- 当多个配置区域绑定到相同属性的时候非常有用
- 大小写敏感
考虑下面的 appsettings.json 文件:
{ "TopItem": { "Month": { "Name": "Green Widget", "Model": "GW46" }, "Year": { "Name": "Orange Gadget", "Model": "OG35" } } }
使用下面的类绑定到 TopItem:Month 和 TopItem:Year,而不是创建两个不同的类:
public class TopItemSettings { public const string Month = "Month"; public const string Year = "Year"; public string Name { get; set; } public string Model { get; set; } }
下面的代码配置命名选项:
public void ConfigureServices(IServiceCollection services) { services.Configure<TopItemSettings>(TopItemSettings.Month, Configuration.GetSection("TopItem:Month")); services.Configure<TopItemSettings>(TopItemSettings.Year, Configuration.GetSection("TopItem:Year")); services.AddRazorPages(); }
下面的代码输出显示命名选项:
public class TestNOModel : PageModel { private readonly TopItemSettings _monthTopItem; private readonly TopItemSettings _yearTopItem; public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor) { _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month); _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year); } public ContentResult OnGet() { return Content($"Month:Name {_monthTopItem.Name} \n" + $"Month:Model {_monthTopItem.Model} \n\n" + $"Year:Name {_yearTopItem.Name} \n" + $"Year:Model {_yearTopItem.Model} \n" ); } }
所有的选项都是命名的实例。IConfigureOptions<TOptions> 实例被当做目标为 Options.DefaultName 的实例。IConfigureNamedOptions<TOptions> 也实现 IConfigureOptions<TOptions>。IOptionsFactory<TOptions> 的默认实现适当使用每个的逻辑。null 命名选项被用来指向所有的命名实例,而不是特定的命名实例。ConfigureAll 和 PostConfigureAll 使用这个约定。
OptionsBuider 在 Options validation 部分有被使用到。
使用依赖注入(DI)服务配置选项
通过两种方式配置选项,服务可以通过依赖注入访问:
- 传递一个配置代理到 Configure 的 OptionsBuilder<TOptions> 中
OptionsBuilder<TOptions> 提供了一个 Configure 的重载允许使用至多 5 个服务配置选项:
services.AddOptions<MyOptions>("optionalName") .Configure<Service1, Service2, Service3, Service4, Service5>( (o, s, s2, s3, s4, s5) => o.Property = DoSomethingWith(s, s2, s3, s4, s5));
- 创建一个实现 IConfigureOptions<TOptions> 或 IConfigureNamedOptions<TOptions> 的类型,注册为一个服务。
我们建议传递一个配置代理给 Configure,因为创建一个服务更加复杂。创建一个类型相当于框架调用 Configure 时做的事情。调用 Configure 注册一个短暂的泛型 IConfigureNamedOptions<TOptions>,它有一个接受特定泛型类型的服务。
选项验证
选项验证使得选项的值可以被验证。
考虑下面的 appsettings.json 文件:
{ "MyConfig": { "Key1": "My Key One", "Key2": 10, "Key3": 32 } }
下面的类绑定到 "MyConfig" 配置区域,应用了一组 DataAnnotations 规则:
public class MyConfigOptions { public const string MyConfig = "MyConfig"; [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")] public string Key1 { get; set; } [Range(0, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")] public int Key2 { get; set; } public int Key3 { get; set; } }
下面的代码:
- 调用 AddOptions 获取到一个 OptionsBuilder<TOptions>,用来绑定 MyConfigOptions 类
- 调用 ValidateDataAnnotations 使能使用 DataAnnotations 的验证
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions<MyConfigOptions>() .Bind(Configuration.GetSection(MyConfigOptions.MyConfig)) .ValidateDataAnnotations(); services.AddControllersWithViews(); }
扩展方法 ValidateDataAnnotations 定义在 Microsoft.Extensions.Options.DataAnnotations NuGet 包中。对于使用 Microsoft.NET.Sdk.Web 的 web 应用程序,这个包会从共享框架中自动引用。
下面的代码输出了配置的值或验证的错误信息:
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; private readonly IOptions<MyConfigOptions> _config; public HomeController(IOptions<MyConfigOptions> config, ILogger<HomeController> logger) { _config = config; _logger = logger; try { var configValue = _config.Value; } catch (OptionsValidationException ex) { foreach (var failure in ex.Failures) { _logger.LogError(failure); } } } public ContentResult Index() { string msg; try { msg = $"Key1: {_config.Value.Key1} \n" + $"Key2: {_config.Value.Key2} \n" + $"Key3: {_config.Value.Key3}"; } catch (OptionsValidationException optValEx) { return Content(optValEx.Message); } return Content(msg); }
下面的代码使用代理应用了一个更加复杂的验证规则:
public void ConfigureServices(IServiceCollection services) { services.AddOptions<MyConfigOptions>() .Bind(Configuration.GetSection(MyConfigOptions.MyConfig)) .ValidateDataAnnotations() .Validate(config => { if (config.Key2 != 0) { return config.Key3 > config.Key2; } return true; }, "Key3 must be > than Key2."); // Failure message. services.AddControllersWithViews(); }
IValidateOptions 用来实现复杂的验证
下面的类实现了 IValidateOptions<TOptions>:
public class MyConfigValidation : IValidateOptions<MyConfigOptions> { public MyConfigOptions _config { get; private set; } public MyConfigValidation(IConfiguration config) { _config = config.GetSection(MyConfigOptions.MyConfig) .Get<MyConfigOptions>(); } public ValidateOptionsResult Validate(string name, MyConfigOptions options) { string vor=null; var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$"); var match = rx.Match(options.Key1); if (string.IsNullOrEmpty(match.Value)) { vor = $"{options.Key1} doesn't match RegEx \n"; } if ( options.Key2 < 0 || options.Key2 > 1000) { vor = $"{options.Key2} doesn't match Range 0 - 1000 \n"; } if (_config.Key2 != default) { if(_config.Key3 <= _config.Key2) { vor += "Key3 must be > than Key2."; } } if (vor != null) { return ValidateOptionsResult.Fail(vor); } return ValidateOptionsResult.Success; } }
IValidateOptions 使得能够把验证代码从 StartUp 中移出来,放在一个类中。
使用前面的代码,下面的代码在 Startup.ConfigureServices 中使能验证:
public void ConfigureServices(IServiceCollection services) { services.Configure<MyConfigOptions>(Configuration.GetSection( MyConfigOptions.MyConfig)); services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions <MyConfigOptions>, MyConfigValidation>()); services.AddControllersWithViews(); }
选项 post-configuration
使用 IPostConfigureOptions<TOptions> 设置 post-configuration。Post-configuration 在所有 IConfigureOptions<TOptions> 配置之后运行:
services.PostConfigure<MyOptions>(myOptions => { myOptions.Option1 = "post_configured_option1_value"; });
PostConfigure 对于 post-configure 命名选项是可用的:
services.PostConfigure<MyOptions>("named_options_1", myOptions => { myOptions.Option1 = "post_configured_option1_value"; });
使用 PostConfigureAll post-configure 所有的配置实例:
services.PostConfigureAll<MyOptions>(myOptions => { myOptions.Option1 = "post_configured_option1_value"; });
在 startup 中访问选项
IOptions<TOptions> 和 IOptionsMonitor<TOptions> 可以在 Startup.Configure 中使用,因为服务是在 Configure 方法执行之前调用的。
public void Configure(IApplicationBuilder app, IOptionsMonitor<MyOptions> optionsAccessor) { var option1 = optionsAccessor.CurrentValue.Option1; }
不要在 Startup.ConfigureServices 中使用 IOptions<TOptions> 或者 IOptionsMonitor<TOptions>。由于服务注册的顺序,不一致的选项状态可能会存在。
Options.ConfigurationExtensions NuGet package
Microsoft.Extensions.Options.ConfigurationExtensions 包会在 ASP.NET Core 应用程序中隐式引用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端