选项模式使用类来提供对相关设置组的强类型访问。 当配置设置由方案隔离到单独的类时,应用遵循两个重要软件工程原则:

  • 封装:依赖于配置设置的类仅依赖于其使用的配置设置
  • 分离关注点:应用的不同部件的设置不彼此依赖或相互耦合

绑定分层配置

  读取相关配置值的首选方法是使用选项模式。 例如,若要读取以下配置值,请执行以下操作:

1
2
3
4
5
6
{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
}

  选项类:

  • 必须是包含公共无参数构造函数的非抽象类。
  • 类型的所有公共读写属性都已绑定。
  • 字段不是绑定的。 在下面的代码中,Position 未绑定。 由于使用了 Position 字段,因此在将类绑定到配置提供程序时,不需要在应用中对字符串 “Position” 进行硬编码。
1
2
3
4
5
6
7
public class PositionOptions
{
public const string Position = "Position";

public string Title { get; set; } = String.Empty;
public string Name { get; set; } = String.Empty;
}

  通过以下代码:

  • 调用 ConfigurationBinder.Bind 将 PositionOptions 类绑定到 Position 部分。
  • 显式 Position 配置数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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}");
}
}

  ConfigurationBinder.Get<T> 绑定并返回指定的类型。 使用 ConfigurationBinder.Get<T> 可能比使用 ConfigurationBinder.Bind 更方便。 下面的代码演示如何将 ConfigurationBinder.Get<T>PositionOptions 类配合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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}");
}
}

  使用选项模式时的另一种方法是绑定 Position 部分,并将其添加到依赖关系注入服务容器中。 在以下代码中,PositionOptions 已通过 Configure 被添加到了服务容器并已绑定到了配置:

1
2
3
4
5
6
7
8
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

  不会读取在应用启动后对 JSON 配置文件所做的更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 配置文件所做的更改,使用默认配置时,会通过 reloadOnChange: true 启用 appsettings.json 和 appsettings.{Environment}.json 文件。 应用启动后,对 appsettings.json 和 appsettings.{Environment}.json 文件所做的更改将由 JSON 配置提供程序读取。

  • 针对请求生存期访问和缓存选项时,每个请求只能计算一次选项。
  • 可能会产生显著的性能损失,因为它是范围内服务,并按请求重新计算。 有关详细信息,请参阅此 GitHub 问题和改进配置绑定的性能。
  • 当使用支持读取已更新的配置值的配置提供程序时,将在应用启动后读取对配置所做的更改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestSnapModel : PageModel
{
private readonly PositionOptions _options;

public TestSnapModel(IOptionsSnapshot<PositionOptions> options)
{
_options = options.Value;
}

public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}

  会读取在应用启动后对 JSON 配置文件所做的更改,使用默认配置时,会通过 reloadOnChange: true 启用 appsettings.json 和 appsettings.{Environment}.json 文件。 应用启动后,对 appsettings.json 和 appsettings.{Environment}.json 文件所做的更改将由 JSON 配置提供程序读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestSnapModel : PageModel
{
private readonly IOptionsMonitor<PositionOptions> _options;

public TestSnapModel(IOptionsMonitor<PositionOptions> options)
{
_options = options;
}

public ContentResult OnGet()
{
return Content($"Title: {_options.CurrentValue.Title} \n" +
$"Name: {_options.CurrentValue.Name}");
}
}

  IOptionsMonitorIOptionsSnapshot 之间的区别在于:

  • IOptionsMonitor 是一种单一示例服务,可随时检索当前选项值,这在单一实例依赖项中尤其有用。
  • IOptionsSnapshot 是一种范围服务,并在构造 IOptionsSnapshot 对象时提供选项的快照。 选项快照旨在用于暂时性和有作用域的依赖项。
特性 IOptionsMonitor IOptionsSnapshot
作用范围 单例(Singleton) 请求范围(Scoped)
配置更新频率 实时更新,每次读取时获取最新值 每个请求获取一次,在整个请求期间保持相同的值
适用场景 适用于长期运行的单例服务,需要动态更新配置 适用于请求范围的场景,如控制器和短生命周期服务
是否可监控配置变化 可以使用 OnChange 监控配置变化 不支持自动监控配置变化
适用于 单例服务、动态更新场景 HTTP请求生命周期中的服务

选项接口

  • IOptions<TOptions>
    • 不支持:
      • 在应用启动后读取配置数据。
      • 命名选项
    • 注册为单一实例且可以注入到任何服务生存期。
  • IOptionsSnapshot<TOptions>
    • 在每次请求时应重新计算选项的方案中有用。
    • 注册为范围内,因此无法注入到单一实例服务。
    • 支持命名选项
  • IOptionsMonitor<TOptions>
    • 用于检索选项并管理 TOptions 实例的选项通知。
    • 注册为单一实例且可以注入到任何服务生存期。
    • 支持:
      • 更改通知
      • 命名选项
      • 可重载配置
      • 选择性选项失效 (IOptionsMonitorCache<TOptions>)

  后期配置方案允许在进行所有 IConfigureOptions<TOptions> 配置后设置或更改选项。

  IOptionsFactory<TOptions> 负责新建选项实例。 它具有单个 Create 方法。 默认实现采用所有已注册 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions> 并首先运行所有配置,然后才进行后期配置。 它区分 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions> 且仅调用适当的接口。

  IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions> 用于缓存 TOptions 实例。 IOptionsMonitorCache<TOptions> 可使监视器中的选项实例无效,以便重新计算值 (TryRemove)。 可以通过 TryAdd 手动引入值。 在应按需重新创建所有命名实例时使用 Clear 方法。

IConfigureNamedOptions

  命名选项:

  • 当多个配置节绑定到同一属性时有用
  • 区分大小写
1
2
3
4
5
6
7
8
9
10
11
12
 {
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
}
1
2
3
4
5
6
7
8
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";

public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}
1
2
3
4
5
6
7
8
9
10
11
12
using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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" );
}
}

OptionsBuilderAPI

  OptionsBuilder<TOptions> 用于配置 TOptions 实例。 OptionsBuilder 简化了创建命名选项的过程,因为它只是初始 AddOptions<TOptions>(string optionsName) 调用的单个参数,而不会出现在所有后续调用中。 选项验证和接受服务依赖关系的 ConfigureOptions 重载仅可通过 OptionsBuilder 获得。

DI服务配置选项

  在配置选项时,可以通过以下两种方式通过依赖关系注入访问服务:

  • 将配置委托传递给 OptionsBuilder<TOptions> 上的 ConfigureOptionsBuilder<TOptions> 提供 Configure 的重载,该重载允许使用最多五个服务来配置选项:
1
2
3
4
builder.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>,它具有可接受指定的通用服务类型的构造函数。

选项验证

  通过选项验证,可以验证选项值。

1
2
3
4
5
6
7
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
1
2
3
4
5
6
7
8
9
10
11
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 以获取绑定到 MyConfigOptions 类的 OptionsBuilder<TOptions>
  • 调用 ValidateDataAnnotations 以使用 DataAnnotations 启用验证。
1
2
3
4
5
6
7
8
9
10
11
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();

var app = builder.Build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
}

  下面的代码使用委托应用更复杂的验证规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.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.

var app = builder.Build();

IValidateOptions

  下面的类实现了IvalidateOptions<TOptions>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));

// 注入自定义选项验证服务
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

IValidatableObject

  IValidatableObject 通常用于模型类的验证。它允许你定义自定义的验证逻辑,通常与数据注释(DataAnnotations)一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

public class Person : IValidatableObject
{
[Required]
public string Name { get; set; }

public int Age { get; set; }

// 实现 IValidatableObject
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Age < 18)
{
yield return new ValidationResult("Age must be at least 18.", new[] { nameof(Age) });
}
}
}

ValidateOnStart

  选项验证将在第一次创建 IOptions<TOptions>IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions> 实现时运行。 若要立即运行选项验证,请在应用启动时调用 Program.cs 中的 ValidateOnStart

1
2
3
4
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();

选项后期配置

  使用 IPostConfigureOptions<TOptions> 设置后期配置。 进行所有 IConfigureOptions<TOptions> 配置后运行后期配置。用于在所有配置源都应用后,对选项对象进行进一步配置或修改的机制。它通常用于在其他配置(如 Configure 或外部配置文件绑定)完成后,执行某些额外的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure 可用于对命名选项进行后期配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

使用 PostConfigureAll 对所有配置实例进行后期配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});

访问 Program.cs 中的选项

  若要访问 Program.cs 中的 IOptions<TOptions>IOptionsMonitor<TOptions>,请在 WebApplication.Services 上调用 GetRequiredService

1
2
3
4
var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;