microsoft-extensions-configuration
IValidateOptionsなどのMicrosoft.Extensions.Optionsパターンを活用し、厳密に型指定された設定や起動時の検証、Optionsパターンを用いて、設定管理を効率化し、より使いやすくするSkill。
📜 元の英語説明(参考)
Microsoft.Extensions.Options patterns including IValidateOptions, strongly-typed settings, validation on startup, and the Options pattern for clean configuration management.
🇯🇵 日本人クリエイター向け解説
IValidateOptionsなどのMicrosoft.Extensions.Optionsパターンを活用し、厳密に型指定された設定や起動時の検証、Optionsパターンを用いて、設定管理を効率化し、より使いやすくするSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o microsoft-extensions-configuration.zip https://jpskill.com/download/8715.zip && unzip -o microsoft-extensions-configuration.zip && rm microsoft-extensions-configuration.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/8715.zip -OutFile "$d\microsoft-extensions-configuration.zip"; Expand-Archive "$d\microsoft-extensions-configuration.zip" -DestinationPath $d -Force; ri "$d\microsoft-extensions-configuration.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
microsoft-extensions-configuration.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
microsoft-extensions-configurationフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
Microsoft.Extensions Configuration パターン
この Skill を使用する場面
この Skill は、以下のような場合に使用します。
- appsettings.json から厳密に型指定されたクラスに構成をバインドする
- アプリケーションの起動時に構成を検証する (フェイルファスト)
- 設定に対して複雑な検証ロジックを実装する
- テスト可能で保守可能な構成クラスを設計する
- IOptions<T>、IOptionsSnapshot<T>、および IOptionsMonitor<T> を理解する
構成の検証が重要な理由
問題点: アプリケーションは、接続文字列の欠落、無効な URL、範囲外の値などの構成ミスにより、実行時に失敗することがよくあります。これらのエラーは、構成がロードされる場所から遠く離れたビジネスロジックの奥深くで発生するため、デバッグが困難になります。
解決策: 起動時に構成を検証します。構成が無効な場合、アプリケーションは明確なエラーメッセージとともにすぐに失敗します。これが「フェイルファスト」の原則です。
// BAD: 誰かがサービスを使用しようとすると、実行時に失敗する
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// 本番環境で 10 分後に NullReferenceException をスローする
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// GOOD: 明確なエラーで起動時に失敗する
// "SmtpSettings validation failed: Host is required"
パターン 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;
}
構成からバインドする
// Program.cs またはサービス登録内
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"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 host is required")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Port must be between 1 and 65535")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "Username must be a valid email address")]
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<T>
データアノテーションは単純なルールには有効ですが、複雑な検証には IValidateOptions<T> が必要です。
IValidateOptions を使用する場面
| シナリオ | データアノテーション | 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 is required");
}
// 範囲の検証
if (options.Port is < 1 or > 65535)
{
failures.Add($"Port {options.Port} is invalid. Must be between 1 and 65535");
}
// プロパティ間の検証
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
{
failures.Add("Password is required when Username is specified");
}
// 条件付き検証
if (options.UseSsl && options.Port == 25)
{
failures.Add("Port 25 is typically not used with SSL. Consider port 465 or 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 バリデーターが実行されます。すべてのエラーが収集され、まとめて報告されます。
パターン 4: 依存関係を持つバリデーター
IValidateOptions バリデーターは DI から解決されるため、依存関係を持つことができます。
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readon 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Microsoft.Extensions Configuration Patterns
When to Use This Skill
Use this skill when:
- Binding configuration from appsettings.json to strongly-typed classes
- Validating configuration at application startup (fail fast)
- Implementing complex validation logic for settings
- Designing configuration classes that are testable and maintainable
- Understanding IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>
Why Configuration Validation Matters
The Problem: Applications often fail at runtime due to misconfiguration - missing connection strings, invalid URLs, out-of-range values. These failures happen deep in business logic, far from where configuration is loaded, making debugging difficult.
The Solution: Validate configuration at startup. If configuration is invalid, the application fails immediately with a clear error message. This is the "fail fast" principle.
// BAD: Fails at runtime when someone tries to use the service
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// Throws NullReferenceException 10 minutes into production
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// GOOD: Fails at startup with clear error
// "SmtpSettings validation failed: Host is required"
Pattern 1: Basic Options Binding
Define a Settings Class
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;
}
Bind from Configuration
// In Program.cs or service registration
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "user@example.com",
"Password": "secret",
"UseSsl": true
}
}
Consume in Services
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - singleton, read once at startup
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
Pattern 2: Data Annotations Validation
For simple validation rules, use Data Annotations:
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP host is required")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Port must be between 1 and 65535")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "Username must be a valid email address")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
Enable Data Annotations Validation
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Enable attribute-based validation
.ValidateOnStart(); // Validate immediately at startup
Key Point: .ValidateOnStart() is critical. Without it, validation only runs when the options are first accessed, which could be minutes or hours into application runtime.
Pattern 3: IValidateOptions<T> for Complex Validation
Data Annotations work for simple rules, but complex validation requires IValidateOptions<T>:
When to Use IValidateOptions
| Scenario | Data Annotations | IValidateOptions |
|---|---|---|
| Required field | ✅ | ✅ |
| Range check | ✅ | ✅ |
| Regex pattern | ✅ | ✅ |
| Cross-property validation | ❌ | ✅ |
| Conditional validation | ❌ | ✅ |
| External service checks | ❌ | ✅ |
| Custom error messages with context | Limited | ✅ |
| Dependency injection in validator | ❌ | ✅ |
Implementing IValidateOptions
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
// Required field validation
if (string.IsNullOrWhiteSpace(options.Host))
{
failures.Add("Host is required");
}
// Range validation
if (options.Port is < 1 or > 65535)
{
failures.Add($"Port {options.Port} is invalid. Must be between 1 and 65535");
}
// Cross-property validation
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
{
failures.Add("Password is required when Username is specified");
}
// Conditional validation
if (options.UseSsl && options.Port == 25)
{
failures.Add("Port 25 is typically not used with SSL. Consider port 465 or 587");
}
// Return result
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
Register the Validator
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Run attribute validation first
.ValidateOnStart();
// Register the custom validator
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();
Order matters: Data Annotations run first, then IValidateOptions validators. All failures are collected and reported together.
Pattern 4: Validators with Dependencies
IValidateOptions validators are resolved from DI, so they can have dependencies:
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readonly ILogger<DatabaseSettingsValidator> _logger;
private readonly IHostEnvironment _environment;
public DatabaseSettingsValidator(
ILogger<DatabaseSettingsValidator> logger,
IHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add("ConnectionString is required");
}
// Environment-specific validation
if (_environment.IsProduction())
{
if (options.ConnectionString?.Contains("localhost") == true)
{
failures.Add("Production cannot use localhost database");
}
if (!options.ConnectionString?.Contains("Encrypt=True") == true)
{
_logger.LogWarning("Production database connection should use encryption");
}
}
// Validate connection string format
if (!string.IsNullOrEmpty(options.ConnectionString))
{
try
{
var builder = new SqlConnectionStringBuilder(options.ConnectionString);
if (string.IsNullOrEmpty(builder.DataSource))
{
failures.Add("ConnectionString must specify a Data Source");
}
}
catch (Exception ex)
{
failures.Add($"ConnectionString is malformed: {ex.Message}");
}
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
Pattern 5: Named Options
When you have multiple instances of the same settings type (e.g., multiple database connections):
// appsettings.json
{
"Databases": {
"Primary": {
"ConnectionString": "Server=primary;..."
},
"Replica": {
"ConnectionString": "Server=replica;..."
}
}
}
// Registration
builder.Services.AddOptions<DatabaseSettings>("Primary")
.BindConfiguration("Databases:Primary")
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<DatabaseSettings>("Replica")
.BindConfiguration("Databases:Replica")
.ValidateDataAnnotations()
.ValidateOnStart();
// Consumption
public class DataService
{
private readonly DatabaseSettings _primary;
private readonly DatabaseSettings _replica;
public DataService(IOptionsSnapshot<DatabaseSettings> options)
{
_primary = options.Get("Primary");
_replica = options.Get("Replica");
}
}
Named Options Validator
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
{
var failures = new List<string>();
var prefix = string.IsNullOrEmpty(name) ? "" : $"[{name}] ";
if (string.IsNullOrWhiteSpace(options.ConnectionString))
{
failures.Add($"{prefix}ConnectionString is required");
}
// Name-specific validation
if (name == "Primary" && options.ReadOnly)
{
failures.Add("Primary database cannot be read-only");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
Pattern 6: Options Lifetime
Understanding the three options interfaces:
| Interface | Lifetime | Reloads on Change | Use Case |
|---|---|---|---|
IOptions<T> |
Singleton | No | Static config, read once |
IOptionsSnapshot<T> |
Scoped | Yes (per request) | Web apps needing fresh config |
IOptionsMonitor<T> |
Singleton | Yes (with callback) | Background services, real-time updates |
IOptionsMonitor for Background Services
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
// Subscribe to configuration changes
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
_logger.LogInformation("Worker settings updated: Interval={Interval}",
settings.PollingInterval);
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}
Pattern 7: Post-Configuration
Modify options after binding but before validation:
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
// Ensure BaseUrl ends with /
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
{
options.BaseUrl += '/';
}
// Set defaults based on environment
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();
PostConfigure with Dependencies
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure<IHostEnvironment>((options, env) =>
{
if (env.IsDevelopment())
{
options.Timeout = TimeSpan.FromMinutes(5); // Longer timeout for debugging
}
});
Pattern 8: Complete Example - Production Settings Class
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
public class AkkaSettings
{
public const string SectionName = "AkkaSettings";
[Required]
public string ActorSystemName { get; set; } = "MySystem";
public AkkaExecutionMode ExecutionMode { get; set; } = AkkaExecutionMode.LocalTest;
public bool LogConfigOnStart { get; set; } = false;
public RemoteOptions RemoteOptions { get; set; } = new();
public ClusterOptions ClusterOptions { get; set; } = new();
public ClusterBootstrapOptions ClusterBootstrapOptions { get; set; } = new();
}
public enum AkkaExecutionMode
{
LocalTest, // No remoting, no clustering
Clustered // Full cluster with sharding, distributed pub/sub
}
public class AkkaSettingsValidator : IValidateOptions<AkkaSettings>
{
private readonly IHostEnvironment _environment;
public AkkaSettingsValidator(IHostEnvironment environment)
{
_environment = environment;
}
public ValidateOptionsResult Validate(string? name, AkkaSettings options)
{
var failures = new List<string>();
// Basic validation
if (string.IsNullOrWhiteSpace(options.ActorSystemName))
{
failures.Add("ActorSystemName is required");
}
// Mode-specific validation
if (options.ExecutionMode == AkkaExecutionMode.Clustered)
{
ValidateClusteredMode(options, failures);
}
// Environment-specific validation
if (_environment.IsProduction() && options.ExecutionMode == AkkaExecutionMode.LocalTest)
{
failures.Add("LocalTest execution mode is not allowed in production");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
private void ValidateClusteredMode(AkkaSettings options, List<string> failures)
{
if (string.IsNullOrEmpty(options.RemoteOptions.PublicHostName))
{
failures.Add("RemoteOptions.PublicHostName is required in Clustered mode");
}
if (options.RemoteOptions.Port is null or < 0)
{
failures.Add("RemoteOptions.Port must be >= 0 in Clustered mode");
}
if (options.ClusterBootstrapOptions.Enabled)
{
ValidateClusterBootstrap(options.ClusterBootstrapOptions, failures);
}
else if (options.ClusterOptions.SeedNodes?.Length == 0)
{
failures.Add("Either ClusterBootstrap must be enabled or SeedNodes must be specified");
}
}
private void ValidateClusterBootstrap(ClusterBootstrapOptions options, List<string> failures)
{
if (string.IsNullOrEmpty(options.ServiceName))
{
failures.Add("ClusterBootstrapOptions.ServiceName is required");
}
if (options.RequiredContactPointsNr <= 0)
{
failures.Add("ClusterBootstrapOptions.RequiredContactPointsNr must be > 0");
}
switch (options.DiscoveryMethod)
{
case DiscoveryMethod.Config:
if (options.ConfigServiceEndpoints?.Length == 0)
{
failures.Add("ConfigServiceEndpoints required for Config discovery");
}
break;
case DiscoveryMethod.AzureTableStorage:
if (options.AzureDiscoveryOptions == null)
{
failures.Add("AzureDiscoveryOptions required for Azure discovery");
}
break;
}
}
}
// Registration
builder.Services.AddOptions<AkkaSettings>()
.BindConfiguration(AkkaSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<AkkaSettings>, AkkaSettingsValidator>();
Anti-Patterns to Avoid
1. Manual Configuration Access
// BAD: Bypasses validation, hard to test
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // No validation!
}
}
// GOOD: Strongly-typed, validated
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // Validated at startup
}
}
2. Validation in Constructor
// BAD: Validation happens at runtime, not startup
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Required is missing"); // Too late!
}
}
// GOOD: Validation at startup
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();
3. Forgetting ValidateOnStart
// BAD: Validation only runs when first accessed
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // Missing ValidateOnStart!
// GOOD: Fails immediately if invalid
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();
4. Throwing in IValidateOptions
// BAD: Throws exception, breaks validation chain
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("Value cannot be negative"); // Wrong!
return ValidateOptionsResult.Success;
}
// GOOD: Return failure result
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("Value cannot be negative");
return ValidateOptionsResult.Success;
}
Testing Configuration Validators
public class SmtpSettingsValidatorTests
{
private readonly SmtpSettingsValidator _validator = new();
[Fact]
public void Validate_WithValidSettings_ReturnsSuccess()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Port = 587,
Username = "user@example.com",
Password = "secret"
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeTrue();
}
[Fact]
public void Validate_WithMissingHost_ReturnsFail()
{
var settings = new SmtpSettings { Host = "" };
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Host is required");
}
[Fact]
public void Validate_WithUsernameButNoPassword_ReturnsFail()
{
var settings = new SmtpSettings
{
Host = "smtp.example.com",
Username = "user@example.com",
Password = null // Missing!
};
var result = _validator.Validate(null, settings);
result.Succeeded.Should().BeFalse();
result.FailureMessage.Should().Contain("Password is required");
}
}
Summary
| Principle | Implementation |
|---|---|
| Fail fast | .ValidateOnStart() |
| Strongly-typed | Bind to POCO classes |
| Simple validation | Data Annotations |
| Complex validation | IValidateOptions<T> |
| Cross-property rules | IValidateOptions<T> |
| Environment-aware | Inject IHostEnvironment |
| Testable | Validators are plain classes |