.NET でのオプション パターン
オプション パターンは、クラスを使用して、関連する設定のグループに、厳密に型指定されたアクセスを提供します。 構成設定がシナリオ別に個々のクラスに分離されるとき、アプリは次の 2 つの重要なソフトウェア エンジニアリング原則に従います。
- インターフェイス分離の原則 (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; }
}
オプション パターンを使用する場合、オプション クラスは次のとおりです。
- パラメーターなしのパブリック コンストラクターを持った非抽象でなければなりません
- バインドする読み取り/書き込みのパブリック プロパティが含まれます (フィールドはバインド "されません")
次のコードは、C# ファイル Program.cs の一部であり、次のことを行います。
- 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 クラスにより、class
に制約 "ConfigurationBinder" API がいくつか公開されます (.Bind(object instance)
や .Get<T>()
など)。 いずれかのオプション インターフェイスを使用するときは、前に説明したオプション クラスの制約に従う必要があります。
オプション パターンを使用するときのもう 1 つの方法は、"TransientFaultHandlingOptions"
セクションをバインドし、それを"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 を使用して更新データを読み取る」を参照してください。
- スコープとして登録されているため、シングルトン サービスには挿入できません。
- 名前付きオプションをサポートします。
- オプションを取得し、
TOptions
インスタンスのオプション通知を管理するために使用されます。 - シングルトンとして登録されており、任意のサービスの有効期間に挿入できます。
- サポートするものは次のとおりです。
- 変更通知
- 名前付きオプション
- 再読み込み可能な構成
- 選択的なオプションの無効化 (IOptionsMonitorCache<TOptions>)
IOptionsFactory<TOptions> は、新しいオプション インスタンスを作成します。 Create メソッドが 1 つ含まれています。 既定の実装では、登録されている 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> を使用すると、オプションは要求ごとにアクセス時に 1 回計算され、要求の有効期間中キャッシュされます。 更新された構成値の読み取りをサポートする構成プロバイダーを使用しているとき、構成の変更は、アプリの開始後に読み取られます。
IOptionsMonitor
と IOptionsSnapshot
の違いは次のとおりです。
IOptionsMonitor
は常に最新のオプション値を取得するIOptionsMonitor
です。これは、シングルトンの依存関係で特に便利です。IOptionsSnapshot
は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
に設定して、ファイル システムの変更をポーリングします。 変更がポーリングされる間隔は 4 秒ごとであり、構成することはできません。
Docker コンテナーの詳細については、.NET アプリのコンテナー化に関するページを参照してください。
IConfigureNamedOptions を使用した名前付きオプションのサポート
名前付きオプション:
- 複数の構成セクションが同じプロパティにバインドされている場合に便利です。
- 大文字と小文字の区別があります。
以下の appsettings.json ファイルについて考えます:
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
Features:Personalize
と Features:WeatherStation
をバインドする 2 つのクラスを作成するのではなく、各セクションに対して次のクラスを使用します。
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> インスタンスは、string.Empty
である、Options.DefaultName
インスタンスを対象とするものとして処理されます。 IConfigureNamedOptions<TOptions> はまた、IConfigureOptions<TOptions> を実装します。 IOptionsFactory<TOptions> の既定の実装には、それぞれを適切に使用するロジックがあります。 名前付きオプション null
は、特定の名前付きインスタンスではなく、すべての名前付きインスタンスを対象とするために使用されます。 ConfigureAll と PostConfigureAll では、この規則が使用されます。
OptionsBuilder API
OptionsBuilder<TOptions> は、TOptions
インスタンスの構成に使用されます。 OptionsBuilder
は名前付きオプションの作成を簡略化します。これは最初の AddOptions<TOptions>(string optionsName)
の呼び出しに対する 1 つのパラメーターにすぎず、後続のすべての呼び出しが表示されなくなるためです。 サービスの依存関係を受け入れるオプションの検証と ConfigureOptions
のオーバーロードは、OptionsBuilder
を介してのみ可能です。
OptionsBuilder
は、「オプションの検証」セクションで使用されます。
DI サービスを使用してオプションを構成する
オプションを構成する場合は、依存性の挿入を使用して登録済みサービスにアクセスし、それらを使用してオプションを構成できます。 これは、オプションを構成するためにサービスにアクセスする必要がある場合に便利です。 オプションの構成中、次の 2 つの方法で DI からサービスにアクセスできます。
OptionsBuilder<TOptions> で Configure に構成デリゲートを渡します。
OptionsBuilder<TOptions>
からOptionsBuilder<TOptions>
のオーバーロードが与えられます。これにより、最大 5 つのサービスを使用してオプションを構成できます。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
クラスにバインドされる AddOptions を取得します。 - ValidateDataAnnotations を呼び出し、
DataAnnotations
を利用した検証を有効にします。
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";
});
関連項目
.NET