ASP.NET Core 2.2 基礎知識(七)【選項】

風靈使發表於2019-02-19

選項模式使用類來表示相關設定的組。 當配置設定由方案隔離到單獨的類時,應用遵循兩個重要軟體工程原則:

  • 介面分離原則 (ISP) 或封裝– 依賴於配置設定的方案(類)僅依賴於其使用的配置設定。
  • 關注點分離– 應用的不同部件的設定不彼此依賴或相互耦合。

選項還提供驗證配置資料的機制。 有關詳細資訊,請參閱選項驗證部分。

系統必備

引用 Microsoft.AspNetCore.App 元包或將包引用新增到 Microsoft.Extensions.Options.ConfigurationExtensions 包。

選項介面

IOptionsMonitor<TOptions>用於檢索選項並管理 TOptions 例項的選項通知。

IOptionsMonitor<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方法。

IOptionsSnapshot<TOptions> 在每次請求時應重新計算選項的方案中有用。 有關詳細資訊,請參閱通過 IOptionsSnapshot 重新載入配置資料部分。

IOptions<TOptions> 可用於支援選項。 但是,IOptions<TOptions> 不支援前面的 IOptionsMonitor<TOptions> 方案。 你可以在已使用 IOptions<TOptions> 介面的現有框架和庫中繼續使用 IOptions<TOptions>,並且不需要 IOptionsMonitor<TOptions> 提供的方案。

常規選項配置

常規選項配置已作為示例 #1 在示例應用中進行了演示。

選項類必須為包含公共無引數建構函式的非抽象類。 以下類 MyOptions 具有兩種屬性:Option1Option2。 設定預設值為可選,但以下示例中的類建構函式設定了 Option1 的預設值。 Option2 具有通過直接初始化屬性設定的預設值 (Models/MyOptions.cs):

public class MyOptions
{
    public MyOptions()
    {
        // Set default value.
        Option1 = "value1_from_ctor";
    }
    
    public string Option1 { get; set; }
    public int Option2 { get; set; } = 5;
}

MyOptions 類已通過 Configure 新增到服務容器並繫結到配置:

// Example #1: General configuration
// Register the Configuration instance which MyOptions binds against.
services.Configure<MyOptions>(Configuration);

以下頁面模型通過 IOptionsMonitor<TOptions> 使用建構函式依賴關係注入來訪問設定 (Pages/Index.cshtml.cs):

private readonly MyOptions _options;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #1: Simple options
var option1 = _options.Option1;
var option2 = _options.Option2;
SimpleOptions = $"option1 = {option1}, option2 = {option2}";

示例的 appsettings.json 檔案指定 option1option2 的值:

{
  "option1": "value1_from_json",
  "option2": -1,
  "subsection": {
    "suboption1": "subvalue1_from_json",
    "suboption2": 200
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

執行應用時,頁面模型的 OnGet 方法返回顯示選項類值的字串:

option1 = value1_from_json, option2 = -1

備註
使用自定義 ConfigurationBuilder 從設定檔案載入選項配置時,請確認基路徑設定正確:

var configBuilder = new ConfigurationBuilder()
   .SetBasePath(Directory.GetCurrentDirectory())
   .AddJsonFile("appsettings.json", optional: true);
var config = configBuilder.Build();

services.Configure<MyOptions>(config);

通過 xref:Microsoft.AspNetCore.WebHost.CreateDefaultBuilder* 從設定檔案載入選項配置時,不需要顯式設定基路徑。

通過委託配置簡單選項

通過委託配置簡單選項已作為示例 #2 在示例應用中進行了演示。

使用委託設定選項值。 此示例應用使用 MyOptionsWithDelegateConfig 類 (Models/MyOptionsWithDelegateConfig.cs):

public class MyOptionsWithDelegateConfig
{
    public MyOptionsWithDelegateConfig()
    {
        // Set default value.
        Option1 = "value1_from_ctor";
    }
    
    public string Option1 { get; set; }
    public int Option2 { get; set; } = 5;
}

在以下程式碼中,已向服務容器新增第二個 IConfigureOptions<TOptions> 服務。 它通過 MyOptionsWithDelegateConfig 使用委託來配置繫結:

// Example #2: Options bound and configured by a delegate
services.Configure<MyOptionsWithDelegateConfig>(myOptions =>
{
    myOptions.Option1 = "value1_configured_by_delegate";
    myOptions.Option2 = 500;
});

Index.cshtml.cs:

private readonly MyOptionsWithDelegateConfig _optionsWithDelegateConfig;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #2: Options configured by delegate
var delegate_config_option1 = _optionsWithDelegateConfig.Option1;
var delegate_config_option2 = _optionsWithDelegateConfig.Option2;
SimpleOptionsWithDelegateConfig = 
    $"delegate_option1 = {delegate_config_option1}, " +
    $"delegate_option2 = {delegate_config_option2}";

可新增多個配置提供程式。 配置提供程式可從 NuGet 包中獲取,並按照註冊的順序應用。 有關更多資訊,請參見ASP.NET Core 中的配置

每次呼叫 Configure 都會將 IConfigureOptions<TOptions> 服務新增到服務容器。 在前面的示例中,Option1Option2 的值同時在 appsettings.json 中指定,但 Option1Option2 的值被配置的委託替代。

當啟用多個配置服務時,指定的最後一個配置源優於其他源,由其設定配置值。 執行應用時,頁面模型的 OnGet 方法返回顯示選項類值的字串:

delegate_option1 = value1_configured_by_delgate, delegate_option2 = 500

子選項配置

子選項配置已作為示例 #3 在示例應用中進行了演示。

應用應建立適用於應用中特定方案組(類)的選項類。 需要配置值的部分應用應僅有權訪問其使用的配置值。

將選項繫結到配置時,選項型別中的每個屬性都將繫結到窗體 property[:sub-property:] 的配置鍵。 例如,MyOptions.Option1 屬性將繫結到從 appsettings.json 中的 option1 屬性讀取的鍵 Option1

在以下程式碼中,已向服務容器新增第三個 IConfigureOptions<TOptions> 服務。 它將 MySubOptions 繫結到 appsettings.json 檔案的 subsection 部分:

// Example #3: Suboptions
// Bind options using a sub-section of the appsettings.json file.
services.Configure<MySubOptions>(Configuration.GetSection("subsection"));

GetSection 擴充套件方法需要 Microsoft.Extensions.Options.ConfigurationExtensions NuGet 包。 如果應用使用 Microsoft.AspNetCore.App metapackage 元包ASP.NET Core 2.1 或更高版本),將自動包含此包。

示例的 appsettings.json 檔案定義具有 suboption1suboption2 的鍵的 subsection 成員:

{
  "option1": "value1_from_json",
  "option2": -1,
  "subsection": {
    "suboption1": "subvalue1_from_json",
    "suboption2": 200
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

MySubOptions 類將屬性 SubOption1SubOption2 定義為保留選項值 (Models/MySubOptions.cs):

public class MySubOptions
{
    public MySubOptions()
    {
        // Set default values.
        SubOption1 = "value1_from_ctor";
        SubOption2 = 5;
    }
    
    public string SubOption1 { get; set; }
    public int SubOption2 { get; set; }
}

頁面模型的 OnGet 方法返回包含選項值的字串 (Pages/Index.cshtml.cs):

private readonly MySubOptions _subOptions;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #3: Suboptions
var subOption1 = _subOptions.SubOption1;
var subOption2 = _subOptions.SubOption2;
SubOptions = $"subOption1 = {subOption1}, subOption2 = {subOption2}";

執行應用時,OnGet 方法返回顯示子選項類值的字串:

subOption1 = subvalue1_from_json, subOption2 = 200

檢視模型或通過直接檢視注入提供的選項

檢視模型或通過直接檢視注入提供的選項已作為示例 #4 在示例應用中進行了演示。

可在檢視模型中或通過將 IOptionsMonitor<TOptions> 直接注入到檢視 (Pages/Index.cshtml.cs) 來提供選項:

private readonly MyOptions _options;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #4: Bind options directly to the page
MyOptions = _options;

示例應用演示如何使用 @inject 指令注入 IOptionsMonitor<MyOptions>

@page
@model IndexModel
@using Microsoft.Extensions.Options
@inject IOptionsMonitor<MyOptions> OptionsAccessor
@{
    ViewData["Title"] = "Options Sample";
}

<h1>@ViewData["Title"]</h1>

當應用執行時,選項值顯示在呈現的頁面中:
選項值 Option1:value1_from_json 和 Option2:-1 從模型中載入並注入檢視。

通過 IOptionsSnapshot 重新載入配置資料

通過 IOptionsSnapshot<TOptions> 重新載入配置資料已作為示例 #5 在示例應用中進行了演示。

IOptionsSnapshot<TOptions> 支援包含最小處理開銷的重新載入選項。

針對請求生存期訪問和快取選項時,每個請求只能計算一次選項。

以下示例演示如何在更改 appsettings.json (Pages/Index.cshtml.cs) 後建立新的 IOptionsSnapshot<TOptions>。 在更改檔案和重新載入配置之前,針對伺服器的多個請求返回 appsettings.json 檔案提供的常數值。

private readonly MyOptions _snapshotOptions;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #5: Snapshot options
var snapshotOption1 = _snapshotOptions.Option1;
var snapshotOption2 = _snapshotOptions.Option2;
SnapshotOptions = 
    $"snapshot option1 = {snapshotOption1}, " +
    $"snapshot option2 = {snapshotOption2}";

下圖顯示從 appsettings.json 檔案載入的初始 option1option2 值:

snapshot option1 = value1_from_json, snapshot option2 = -1

將 appsettings.json 檔案中的值更改為 value1_from_json UPDATED200。 儲存 appsettings.json 檔案。 重新整理瀏覽器,檢視更新的選項值:

snapshot option1 = value1_from_json UPDATED, snapshot option2 = 200

包含 IConfigureNamedOptions 的命名選項支援

包含 IConfigureNamedOptions<TOptions> 的命名選項支援已作為示例 #6 在示例應用中進行了演示。

命名選項支援允許應用在命名選項配置之間進行區分。 在示例應用中,命名選項通過 OptionsServiceCollectionExtensions.Configure 進行宣告,其呼叫擴充套件方法 ConfigureNamedOptions<TOptions>.Configure

// Example #6: Named options (named_options_1)
// Register the ConfigurationBuilder instance which MyOptions binds against.
// Specify that the options loaded from configuration are named
// "named_options_1".
services.Configure<MyOptions>("named_options_1", Configuration);

// Example #6: Named options (named_options_2)
// Specify that the options loaded from the MyOptions class are named
// "named_options_2".
// Use a delegate to configure option values.
services.Configure<MyOptions>("named_options_2", myOptions =>
{
    myOptions.Option1 = "named_options_2_value1_from_action";
});

示例應用通過 Get(Pages/Index.cshtml.cs) 訪問命名選項:

private readonly MyOptions _named_options_1;
private readonly MyOptions _named_options_2;
public IndexModel(
    IOptionsMonitor<MyOptions> optionsAccessor, 
    IOptionsMonitor<MyOptionsWithDelegateConfig> optionsAccessorWithDelegateConfig, 
    IOptionsMonitor<MySubOptions> subOptionsAccessor, 
    IOptionsSnapshot<MyOptions> snapshotOptionsAccessor, 
    IOptionsSnapshot<MyOptions> namedOptionsAccessor)
{
    _options = optionsAccessor.CurrentValue;
    _optionsWithDelegateConfig = optionsAccessorWithDelegateConfig.CurrentValue;
    _subOptions = subOptionsAccessor.CurrentValue;
    _snapshotOptions = snapshotOptionsAccessor.Value;
    _named_options_1 = namedOptionsAccessor.Get("named_options_1");
    _named_options_2 = namedOptionsAccessor.Get("named_options_2");
}
// Example #6: Named options
var named_options_1 = 
    $"named_options_1: option1 = {_named_options_1.Option1}, " +
    $"option2 = {_named_options_1.Option2}";
var named_options_2 = 
    $"named_options_2: option1 = {_named_options_2.Option1}, " +
    $"option2 = {_named_options_2.Option2}";
NamedOptions = $"{named_options_1} {named_options_2}";

執行示例應用,將返回命名選項:

named_options_1: option1 = value1_from_json, option2 = -1
named_options_2: option1 = named_options_2_value1_from_action, option2 = 5

從配置中提供從 appsettings.json 檔案中載入的 named_options_1 值。 通過以下內容提供 named_options_2 值:

  • 針對 Option1ConfigureServices 中的 named_options_2 委託。
  • MyOptions 類提供的 Option2 的預設值。

使用 ConfigureAll 方法配置所有選項

使用 ConfigureAll方法配置所有選項例項。 以下程式碼將針對包含公共值的所有配置例項配置 Option1。 將以下程式碼手動新增到 Startup.ConfigureServices 方法:

services.ConfigureAll<MyOptions>(myOptions => 
{
    myOptions.Option1 = "ConfigureAll replacement value";
});

新增程式碼後執行示例應用將產生以下結果:

named_options_1: option1 = ConfigureAll replacement value, option2 = -1
named_options_2: option1 = ConfigureAll replacement value, option2 = 5

備註
所有選項都是命名例項。 現有 IConfigureOptions<TOptions> 例項將被視為面向為 string.EmptyOptions.DefaultName 例項。 IConfigureNamedOptions<TOptions> 還可實現 IConfigureOptions<TOptions>IOptionsFactory<TOptions> 的預設實現具有適當地使用每個例項的邏輯。null 命名選項用於面向所有命名例項而不是某一特定命名例項(ConfigureAllPostConfigureAll 使用此約定)。

OptionsBuilder API

OptionsBuilder<TOptions> 用於配置 TOptions 例項。 OptionsBuilder 簡化了建立命名選項的過程,因為它只是初始 AddOptions<TOptions>(string optionsName) 呼叫的單個引數,而不會出現在所有後續呼叫中。 選項驗證和接受服務依賴關係的 ConfigureOptions 過載僅可通過 OptionsBuilder 獲得。

// Options.DefaultName = "" is used.
services.AddOptions<MyOptions>().Configure(o => o.Property = "default");

services.AddOptions<MyOptions>("optionalName")
    .Configure(o => o.Property = "named");

配置 <TOptions、TDep1、...TDep4> 方法

使用 DI 中的服務,通過以樣本方式實現 IConfigure[Named]Options 來配置選項的方法非常詳細。 OptionsBuilder<TOptions>ConfigureOptions 的過載允許使用最多五個服務來配置選項:

services.AddOptions<MyOptions>("optionalName")
    .Configure<Service1, Service2, Service3, Service4, Service5>(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));

過載會註冊臨時通用 IConfigureNamedOptions<TOptions>,它具有接受指定的通用服務型別的建構函式。

選項驗證

藉助選項驗證,可以驗證已配置的選項。 使用驗證方法呼叫 Validate。如果選項有效,方法返回 true;如果無效,方法返回 false

// Registration
services.AddOptions<MyOptions>("optionalOptionsName")
    .Configure(o => { }) // Configure the options
    .Validate(o => YourValidationShouldReturnTrueIfValid(o), 
        "custom error");

// Consumption
var monitor = services.BuildServiceProvider()
    .GetService<IOptionsMonitor<MyOptions>>();
  
try
{
    var options = monitor.Get("optionalOptionsName");
}
catch (OptionsValidationException e) 
{
   // e.OptionsName returns "optionalOptionsName"
   // e.OptionsType returns typeof(MyOptions)
   // e.Failures returns a list of errors, which would contain 
   //     "custom error"
}

上面的示例將命名選項例項設定為 optionalOptionsName。 預設選項例項為 Options.DefaultName

選項驗證在選項例項建立後執行。 系統保證在選項例項首次獲得訪問時通過驗證。

重要
選項驗證無法防止在最初配置和驗證選項後發生選項修改。

Validate 方法接受 Func<TOptions, bool>。 若要完全自定義驗證,請實現 IValidateOptions<TOptions>,它支援:

  • 驗證多種型別的選項:class ValidateTwo : IValidateOptions<Option1>, IValidationOptions<Option2>
  • 驗證取決於其他選項型別:public DependsOnAnotherOptionValidator(IOptionsMonitor<AnotherOption> options)

IValidateOptions 驗證:

  • 特定的命名選項例項。
  • 所有選項(如果 namenull 的話)。

通過介面實現返回 ValidateOptionsResult

public interface IValidateOptions<TOptions> where TOptions : class
{
    ValidateOptionsResult Validate(string name, TOptions options);
}

通過呼叫 OptionsBuilder<TOptions> 上的 ValidateDataAnnotations 方法,可以從 Microsoft.Extensions.Options.DataAnnotations 包中獲得基於資料註釋的驗證。Microsoft.AspNetCore.App 元包ASP.NET Core 2.2 或更高版本)中包括 Microsoft.Extensions.Options.DataAnnotations

private class AnnotatedOptions
{
    [Required]
    public string Required { get; set; }

    [StringLength(5, ErrorMessage = "Too long.")]
    public string StringLength { get; set; }

    [Range(-5, 5, ErrorMessage = "Out of range.")]
    public int IntRange { get; set; }
}

[Fact]
public void CanValidateDataAnnotations()
{
    var services = new ServiceCollection();
    services.AddOptions<AnnotatedOptions>()
        .Configure(o =>
        {
            o.StringLength = "111111";
            o.IntRange = 10;
            o.Custom = "nowhere";
        })
        .ValidateDataAnnotations();

    var sp = services.BuildServiceProvider();

    var error = Assert.Throws<OptionsValidationException>(() => 
        sp.GetRequiredService<IOptionsMonitor<AnnotatedOptions>>().Value);
    ValidateFailure<AnnotatedOptions>(error, Options.DefaultName, 1,
        "DataAnnotation validation failed for members Required " +
            "with the error 'The Required field is required.'.",
        "DataAnnotation validation failed for members StringLength " +
            "with the error 'Too long.'.",
        "DataAnnotation validation failed for members IntRange " +
            "with the error 'Out of range.'.");
}

設計團隊正在考慮為未來版本提供預先驗證(在啟動時快速失敗)。

選項後期配置

使用 IPostConfigureOptions<TOptions> 設定後期配置。 進行所有 IConfigureOptions<TOptions> 配置後執行後期配置:

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure可用於對命名選項進行後期配置:

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

使用 PostConfigureAll對所有配置例項進行後期配置:

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

在啟動期間訪問選項

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>。 由於服務註冊的順序,可能存在不一致的選項狀態。

相關文章