翻译 - 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" 不用在程序中硬编码

以下代码:

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

选项接口

IOptions<TOptions>

IOptionsSnapshot<TOptions>

IOptionsMonitor<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,因为创建一个服务更加复杂。创建一个类型相当于框架调用 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; }
}

下面的代码:

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 应用程序中隐式引用。

posted @ 2021-03-31 22:30  sims  阅读(276)  评论(0编辑  收藏  举报