Microsoft.Extensions 構成パターン は何をしますか?
このスキルを使用するタイミング
以下の場合にこのスキルを使用します:
- appsettings.json から厳密に型指定されたクラスへの構成のバインド
- アプリケーション起動時の構成検証(フェイルファスト)
- 設定に対する複雑な検証ロジックの実装
- テスト可能で保守可能な構成クラスの設計
- IOptions、IOptionsSnapshot、IOptionsMonitor の理解
参考ファイル
- advanced-patterns.md:依存関係を持つバリデーター、名前付きオプション、完全なプロダクション例(AkkaSettings)、バリデーターのテスト
構成検証が重要な理由
問題点: アプリケーションは、構成ミス(接続文字列の欠落、無効なURL、範囲外の値)により実行時に頻繁に失敗します。これらの失敗は、構成が読み込まれた場所から遠く離れたビジネスロジックの深部で発生します。
解決策: 起動時に構成を検証します。無効な場合は、明確なエラーメッセージとともに即座に失敗します。
// 悪い例:誰かがサービスを使用しようとしたときに実行時に失敗する
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// プロダクション環境で10分後に NullReferenceException をスロー
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// 良い例:起動時に明確なエラーで失敗する
// "SmtpSettings の検証に失敗しました: Host は必須です"
パターン 1:基本的なオプションバインディング
設定クラスの定義
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
構成からのバインド
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "[email protected]",
"Password": "secret",
"UseSsl": true
}
}
サービスでの使用
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - シングルトン、起動時に一度だけ読み取り
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
パターン 2:データ注釈による検証
単純な検証ルールには、データ注釈を使用します:
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP ホストは必須です")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "ポートは 1 から 65535 の間でなければなりません")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "ユーザー名は有効なメールアドレスである必要があります")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
データ注釈検証の有効化
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // 属性ベースの検証を有効化
.ValidateOnStart(); // 起動時に即座に検証
重要なポイント: .ValidateOnStart() は重要です。これがないと、検証はオプションが最初にアクセスされたときのみ実行されます。
パターン 3:複雑な検証のための IValidateOptions
データ注釈は単純なルールに有効ですが、複雑な検証には IValidateOptions<T> が必要です:
| シナリオ | データ注釈 | IValidateOptions |
|---|---|---|
| 必須フィールド | はい | はい |
| 範囲チェック | はい | はい |
| プロパティ間検証 | いいえ | はい |
| 条件付き検証 | いいえ | はい |
| 外部サービスチェック | いいえ | はい |
| バリデーターへの依存性注入 | いいえ | はい |
IValidateOptions の実装
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.Host))
failures.Add("Host は必須です");
if (options.Port is < 1 or > 65535)
failures.Add($"Port {options.Port} は無効です。1 から 65535 の間である必要があります");
// プロパティ間検証
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
failures.Add("ユーザー名が指定されている場合はパスワードが必要です");
// 条件付き検証
if (options.UseSsl && options.Port == 25)
failures.Add("ポート 25 は通常 SSL では使用されません。ポート 465 または 587 を検討してください");
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
バリデーターの登録
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();
順序が重要: データ注釈が最初に実行され、次に IValidateOptions バリデーターが実行されます。すべての失敗がまとめて収集されます。
依存関係を持つバリデーター、名前付きオプション、完全なプロダクション例については、advanced-patterns.md を参照してください。
パターン 4:オプションのライフタイム
| インターフェース | ライフタイム | 変更時の再読み込み | ユースケース |
|---|---|---|---|
IOptions<T> | シングルトン | いいえ | 静的構成、一度だけ読み取り |
IOptionsSnapshot<T> | スコープ | はい(リクエストごと) | 最新の構成が必要なWebアプリ |
IOptionsMonitor<T> | シングルトン | はい(コールバック付き) | バックグラウンドサービス、リアルタイム更新 |
バックグラウンドサービスのための IOptionsMonitor
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}
パターン 5:ポスト構成
バインディング後、検証前にオプションを変更します:
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
options.BaseUrl += '/';
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();
避けるべきアンチパターン
1. 手動による構成アクセス
// 悪い例:検証をバイパスし、テストが困難
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // 検証なし!
}
}
// 良い例:厳密に型指定され、検証済み
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // 起動時に検証済み
}
}
2. コンストラクタ内での検証
// 悪い例:検証が起動時ではなく実行時に発生
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Required がありません"); // 手遅れ!
}
}
// 良い例:IValidateOptions + ValidateOnStart による起動時検証
3. ValidateOnStart の忘れ
// 悪い例:検証は最初にアクセスされたときのみ実行
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // ValidateOnStart が欠落!
// 良い例:無効な場合に即座に失敗
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();
4. IValidateOptions 内での例外スロー
// 悪い例:例外をスローし、検証チェーンを壊す
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("値を負にできません"); // 間違い!
return ValidateOptionsResult.Success;
}
// 良い例:失敗結果を返す
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("値を負にできません");
return ValidateOptionsResult.Success;
}
まとめ
| 原則 | 実装 |
|---|---|
| フェイルファスト | .ValidateOnStart() |
| 厳密な型指定 | POCO クラスにバインド |
| 単純な検証 | データ注釈 |
| 複雑な検証 | IValidateOptions<T> |
| プロパティ間ルール | IValidateOptions<T> |
| 環境認識 | IHostEnvironment の注入 |
| テスト可能性 | バリデーターはプレーンなクラス |