.NET 中的選項模式
選項模式會使用類別來提供相關設定群組的強型別存取。 當組態設定依案例隔離到不同的類別時,應用程式會遵守兩個重要的軟體工程準則:
- 介面隔離準則 (ISP) 或封裝:相依於組態設定的案例 (類別) 只會相依於它們使用的組態設定。
- 關注點分離:應用程式不同部分的設定不會彼此相依或結合。
選項也提供驗證設定資料的機制。 如需詳細資訊,請參閱選項驗證一節。
繫結階層式設定
讀取相關設定值的慣用方式是使用選項模式 (部分機器翻譯)。 選項模式可透過 IOptions<TOptions> 介面實作,其中泛型型別參數 TOptions
限制為 class
。 IOptions<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)));
在上述範例中,builder
是 HostApplicationBuilder 的執行個體。
提示
參數 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 設定檔所做的變更。 若要讀取應用程式啟動之後的變更,請使用 IOptionsSnapshot 或 IOptionsMonitor 來監控這些變更並根據狀況做出反應。
選項介面
- 適用於在有限範圍或暫時性的存留期間,為每個插入解析度重新計算選項的狀況。 如需詳細資訊,請參閱使用 IOptionsSnapshot 讀取更新的資料。
- 註冊為 Scoped 而無法插入 Singleton 服務。
- 支援具名選項
- 用來擷取選項並管理
TOptions
執行個體的選項通知。 - 註冊為 Singleton 且可插入任何服務存留期。
- 支援:
- 變更通知
- 具名選項
- 可重新載入的設定
- 選擇性選項無效判定 (IOptionsMonitorCache<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> 時,在要求的存留期內存取及快取選項時,會針對每個要求計算一次選項。 當使用支援讀取更新設定值的設定提供者時,會在應用程式啟動後讀取設定的變更。
IOptionsMonitor
與 IOptionsSnapshot
之間的差異在於:
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
IOptionsMonitor
類型支持變更通知,並啟用應用程式可能需要動態回應設定來源變更的場景。 這在應用程式啟動後需要對設定資料變更做出反應時非常有用。 變更通知僅支援檔案系統型設定提供者,如下所示:
- Microsoft.Extensions.Configuration.Ini
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.KeyPerFile
- Microsoft.Extensions.Configuration.UserSecrets
- Microsoft.Extensions.Configuration.Xml
若要使用選項監視器,選項物件會以與組態區段相同的方式進行設定。
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
環境變數設為 1
或 true
以輪詢檔案系統中的變更。 輪詢變更的間隔為每隔四秒,而此值無法設定。
如需 Docker 容器的詳細資訊,請參閱將 .NET 應用程式容器化。
使用 IConfigureNamedOptions 的具名選項支援
具名選項:
- 當多個設定區段繫結至相同的屬性時會很有用。
- 會區分大小寫。
以下列 appsettings.json 檔案為例:
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
不要建立兩個類別來繫結 Features:Personalize
和 Features: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 sealed class 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.Empty
。 IConfigureNamedOptions<TOptions> 也會實作 IConfigureOptions<TOptions>。 IOptionsFactory<TOptions> 的預設實作有邏輯可以適當地使用每個項目。 null
具名選項用來以所有具名執行個體為目標,而不是特定的具名執行個體。 ConfigureAll 和 PostConfigureAll 都使用了這個慣例。
OptionsBuilder API
OptionsBuilder<TOptions> 會用於設定 TOptions
執行個體。 因為 OptionsBuilder
僅為初始 AddOptions<TOptions>(string optionsName)
呼叫的單一參數,而不是出現在所有後續呼叫的參數,所以其可簡化建立具名選項的程序。 選項驗證及接受服務依存性的 ConfigureOptions
多載,只可透過 OptionsBuilder
使用。
選項驗證一節所使用的是 OptionsBuilder
。
使用 DI 服務來設定選項
當您設定選項時,您可以使用相依性插入來存取已註冊的服務,並使用它們來設定選項。 當您需要存取服務來設定選項時,這非常有用。 服務可以從 DI 存取,同時以兩種方式設定選項:
將設定委派傳遞到 OptionsBuilder<TOptions> 上的 Configure。
OptionsBuilder<TOptions>
提供 Configure 的多載,可讓您最多使用五個服務來設定選項:builder.Services .AddOptions<MyOptions>("optionalName") .Configure<ExampleService, ScopedService, MonitorService>( (options, es, ss, ms) => options.Property = DoSomethingWith(es, ss, ms));
建立一個能實作 IConfigureOptions<TOptions> 或 IConfigureNamedOptions<TOptions> 的類型,並將該類型註冊為服務。
建議您將設定委派傳遞到 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
的選項物件。 雖然通常都會為兩者取相同的名稱,但這不是必要的動作,而且實際上可能會導致名稱衝突。
下列程式碼範例:
- 呼叫 AddOptions 以取得繫結至
SettingsOptions
類別的 OptionsBuilder<TOptions>。 - 使用
DataAnnotations
來呼叫 ValidateDataAnnotations 以啟用驗證。
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";
});