.NET 中的選項模式

選項模式會使用類別來提供相關設定群組的強型別存取。 當組態設定依案例隔離到不同的類別時,應用程式會遵守兩個重要的軟體工程準則:

選項也提供驗證設定資料的機制。 如需詳細資訊,請參閱選項驗證一節。

繫結階層式設定

讀取相關設定值的慣用方式是使用選項模式 (部分機器翻譯)。 選項模式可透過 IOptions<TOptions> 介面實作,其中泛型型別參數 TOptions 限制為 classIOptions<TOptions> 稍後可透過相依性插入來提供。 如需詳細資訊,請參閱 .NET 的相依性插入

例如,若要從 appsettings.json 檔案中讀取醒目提示的設定值:

{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

建立下列 TransientFaultHandlingOptions 類別:

public sealed class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}

使用選項模式時,選項類別:

  • 必須為非抽象,且具有公用的無參數建構函式
  • 包含要繫結的公用讀寫屬性 (欄位繫結)

下列程式碼是 Program.cs C# 檔案的一部分,而且可以:

  • 呼叫 ConfigurationBinder.Bind 以將 TransientFaultHandlingOptions 類別繫結到 "TransientFaultHandlingOptions" 區段。
  • 顯示 設定資料。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();

IHostEnvironment env = builder.Environment;

builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);

TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
    .Bind(options);

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

// <Output>
// Sample output:

在上述程式碼中,JSON 設定檔的 "TransientFaultHandlingOptions" 區段繫結至 TransientFaultHandlingOptions 執行個體。 這會讓 C# 物件屬性與設定中的對應值結合。

ConfigurationBinder.Get<T> 會繫結並傳回指定的型別。 ConfigurationBinder.Get<T> 可能比使用 ConfigurationBinder.Bind 還方便。 下列程式碼會示範如何搭配 TransientFaultHandlingOptions 類別使用 ConfigurationBinder.Get<T>

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

在上述程式碼中,ConfigurationBinder.Get<T> 用來取得 TransientFaultHandlingOptions 物件的執行個體,而其屬性值則以基礎設定填入。

重要

類別 ConfigurationBinder 會公開數個 API,例如不限class.Bind(object instance).Get<T>()。 使用任何選項介面時,您必須遵守上述選項類別條件約束

使用選項模式的替代方式是繫結 "TransientFaultHandlingOptions" 區段並將其新增至相依性插入服務容器。 在下列程式碼中,TransientFaultHandlingOptions 會使用 Configure 新增到服務容器,並繫結到設定:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

在上述範例中,builderHostApplicationBuilder 的執行個體。

提示

參數 key 是要搜尋的設定區段名稱。 它不一定要符合表示它的型別名稱。 例如,您可以有一個名為 "FaultHandling" 的區段,並且用 TransientFaultHandlingOptions 類別來表示它。 在此執行個體中,您會改將 "FaultHandling" 傳遞至 GetSection 函式。 當具名區段符合其對應類型時,為方便起見會使用 nameof 運算子。

使用上述程式碼,下列程式碼會讀取位置選項:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

在上述程式碼中,不會讀取在應用程式啟動之後對 JSON 設定檔所做的變更。 若要讀取應用程式啟動之後的變更,請使用 IOptionsSnapshotIOptionsMonitor 來監控這些變更並根據狀況做出反應。

選項介面

IOptions<TOptions>

IOptionsSnapshot<TOptions>

IOptionsMonitor<TOptions>

IOptionsFactory<TOptions> 負責建立新的選項執行個體。 它有單一 Create 方法。 預設實作會接受所有已註冊的 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions>,並先執行所有設定,接著執行設定後作業。 它會區別 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>,且只會呼叫適當的介面。

IOptionsMonitorCache<TOptions> 會由 IOptionsMonitor<TOptions> 用來快取 TOptions 執行個體。 IOptionsMonitorCache<TOptions> 會使監視器中的選項執行個體失效,以便重新計算值 (TryRemove)。 值可以使用 TryAdd 手動導入。 Clear 方法用於應該視需要重新建立所有具名執行個體時。

IOptionsChangeTokenSource<TOptions> 可用來擷取追蹤基礎 TOptions 執行個體變更的 IChangeToken。 如需變更權杖基本類型的詳細資訊,請參閱變更通知

選項介面的優點

使用泛型的包裝函式類型可讓您將選項的存留期與 DI 容器分離。 介面 IOptions<TOptions>.Value 會在選項類型上提供抽象層,其中包括泛型條件約束。 這項功能提供了下列優點:

  • T 設定執行個體的評估會延後到存取 IOptions<TOptions>.Value 時進行,而不是插入時。 這一點很重要,因為您可以從各種地方取用 T 選項並選擇存留期語意,而不需要變更任何有關 T 的東西。
  • 註冊類型 T 的選項時,您不需要明確註冊 T 類型。 如果您要撰寫具有簡單預設值的程式庫,而且不想要強迫呼叫端在特定存留期內將選項註冊到 DI 容器的話,這一點會很便利。
  • 從 API 的觀點來看,它允許對類型 T 的條件約束 (在此範例中,T 限制為參考型別)。

使用 IOptionsSnapshot 讀取更新的資料

使用 IOptionsSnapshot<TOptions> 時,在要求的存留期內存取及快取選項時,會針對每個要求計算一次選項。 當使用支援讀取更新設定值的設定提供者時,會在應用程式啟動後讀取設定的變更。

IOptionsMonitorIOptionsSnapshot 之間的差異在於:

  • IOptionsMonitor 是能夠隨時擷取目前選項值的單一資料庫服務,這一點在單一資料庫相依性方面特別有用。
  • IOptionsSnapshot有限範圍服務,會在建構 IOptionsSnapshot<T> 物件時提供選項的快照集。 選項快照集的設計目的是要與暫時性和有限範圍的相依性搭配使用。

下列程式碼會使用 IOptionsSnapshot<TOptions>

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

下列程式碼會註冊 TransientFaultHandlingOptions 繫結的設定執行個體:

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

上述程式碼中使用方法 Configure<TOptions> 來註冊 TOptions 要繫結的設定執行個體,並在設定變更時更新選項。

IOptionsMonitor

若要使用選項監視器,選項物件會以與組態區段相同的方式進行設定。

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

下列範例會使用 IOptionsMonitor<TOptions>

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
    public void DisplayValues()
    {
        TransientFaultHandlingOptions options = monitor.CurrentValue;

        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
    }
}

在上述程式碼中,會讀取在應用程式啟動之後對 JSON 設定檔所做的變更。

提示

某些檔案系統 (例如 Docker 容器和網路共用) 可能無法可靠地傳送變更通知。 在這些環境中的使用 IOptionsMonitor<TOptions> 介面時,請將 DOTNET_USE_POLLING_FILE_WATCHER 環境變數設為 1true 以輪詢檔案系統中的變更。 輪詢變更的間隔為每隔四秒,而此值無法設定。

如需 Docker 容器的詳細資訊,請參閱將 .NET 應用程式容器化

使用 IConfigureNamedOptions 的具名選項支援

具名選項:

  • 當多個設定區段繫結至相同的屬性時會很有用。
  • 會區分大小寫。

以下列 appsettings.json 檔案為例:

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

不要建立兩個類別來繫結 Features:PersonalizeFeatures:WeatherStation,而是為各個區段使用以下類別:

public class Features
{
    public const string Personalize = nameof(Personalize);
    public const string WeatherStation = nameof(WeatherStation);

    public bool Enabled { get; set; }
    public string ApiKey { get; set; }
}

下列程式碼會設定具名選項:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

下列程式碼會顯示具名選項:

public class sealed Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

所有選項都是具名執行個體。 IConfigureOptions<TOptions> 執行個體會視為以 Options.DefaultName 執行個體為目標,也就是 string.EmptyIConfigureNamedOptions<TOptions> 也會實作 IConfigureOptions<TOptions>IOptionsFactory<TOptions> 的預設實作有邏輯可以適當地使用每個項目。 null 具名選項用來以所有具名執行個體為目標,而不是特定的具名執行個體。 ConfigureAllPostConfigureAll 都使用了這個慣例。

OptionsBuilder API

OptionsBuilder<TOptions> 會用於設定 TOptions 執行個體。 因為 OptionsBuilder 僅為初始 AddOptions<TOptions>(string optionsName) 呼叫的單一參數,而不是出現在所有後續呼叫的參數,所以其可簡化建立具名選項的程序。 選項驗證及接受服務依存性的 ConfigureOptions 多載,只可透過 OptionsBuilder 使用。

選項驗證一節所使用的是 OptionsBuilder

使用 DI 服務來設定選項

服務可以透過相依性插入存取,並且以兩種方式設定選項:

我們建議您將設定委派傳遞到 Configure,因為建立服務更複雜。 建立一個類型,相當於在呼叫 Configure 時架構所進行的動作。 呼叫 Configure 會註冊暫時性泛型 IConfigureNamedOptions<TOptions>,其具有會接受所指定泛型服務類型的建構函式。

選項驗證

選項驗證可驗證選項值。

以下列 appsettings.json 檔案為例:

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

下列類別會繫結至 "MyCustomSettingsSection" 設定區段並套用數個 DataAnnotations 規則:

using System.ComponentModel.DataAnnotations;

namespace ConsoleJson.Example;

public sealed class SettingsOptions
{
    public const string ConfigurationSectionName = "MyCustomSettingsSection";

    [Required]
    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public required string SiteTitle { get; set; }

    [Required]
    [Range(0, 1_000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public required int Scale { get; set; }

    [Required]
    public required int VerbosityLevel { get; set; }
}

在上述 SettingsOptions 類別中,ConfigurationSectionName 屬性包含要繫結的設定區段名稱。 在此案例中,選項物件會提供其設定區段的名稱。

提示

設定區段名稱與它要繫結的設定物件無關。 換句話說,名為 "FooBarOptions" 的設定區段可以繫結至名為 ZedOptions 的選項物件。 雖然通常都會為兩者取相同的名稱,但這不是必要的動作,而且實際上可能會導致名稱衝突。

下列程式碼範例:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

ValidateDataAnnotations 擴充方法定義在 Microsoft.Extensions.Options.DataAnnotations NuGet 套件中。

下列程式碼會顯示設定值或報告驗證錯誤:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ValidationService
{
    private readonly ILogger<ValidationService> _logger;
    private readonly IOptions<SettingsOptions> _config;

    public ValidationService(
        ILogger<ValidationService> logger,
        IOptions<SettingsOptions> config)
    {
        _config = config;
        _logger = logger;

        try
        {
            SettingsOptions options = _config.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (string failure in ex.Failures)
            {
                _logger.LogError("Validation error: {FailureMessage}", failure);
            }
        }
    }
}

下列程式碼會使用委派來套用更複雜的驗證規則:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

驗證會在執行階段進行,但您可以改為鏈結對 ValidateOnStart 的呼叫,將其設定為在啟動時發生:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.")
    .ValidateOnStart();

從 .NET 8 開始,您可以使用替代 API AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String),以針對特定選項類型啟用啟動時驗證:

builder.Services
    .AddOptionsWithValidateOnStart<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

IValidateOptions 適用於複雜驗證

下列類別會實作 IValidateOptions<TOptions>

using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

sealed partial class ValidateSettingsOptions(
    IConfiguration config)
    : IValidateOptions<SettingsOptions>
{
    public SettingsOptions? Settings { get; private set; } =
        config.GetSection(SettingsOptions.ConfigurationSectionName)
              .Get<SettingsOptions>();

    public ValidateOptionsResult Validate(string? name, SettingsOptions options)
    {
        StringBuilder? failure = null;
    
        if (!ValidationRegex().IsMatch(options.SiteTitle))
        {
            (failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
        }

        if (options.Scale is < 0 or > 1_000)
        {
            (failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
        }

        if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
        {
            (failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
        }

        return failure is not null
            ? ValidateOptionsResult.Fail(failure.ToString())
            : ValidateOptionsResult.Success;
    }

    [GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
    private static partial Regex ValidationRegex();
}

IValidateOptions 可讓您將驗證程式碼移至一個類別。

注意

此範例程式碼需要 Microsoft.Extensions.Configuration.Json NuGet 套件。

使用上述程式碼時,驗證會在使用下列程式碼設定服務時啟用:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

選項設定後作業

使用 IPostConfigureOptions<TOptions> 來設定設定後作業。 設定後作業會在所有 IConfigureOptions<TOptions> 設定之後執行,在需要覆寫設定的狀況下可能很有用:

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

PostConfigure 可用來後置設定具名選項:

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

使用 PostConfigureAll 後置設定所有設定執行個體:

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

另請參閱