mcpField
M
microsoft-extensions-configuration

microsoft-extensions-configuration

github.com/wshaddix開発86526 回インストール35 週間前に更新
リポジトリ

microsoft-extensions-configuration について

Microsoft.Extensions の設定を扱う .NET 開発者向けのスキルです。型付きバインド、起動時の検証、オプションのライフタイムを通じて、設定ミスを早期に検出します。

主な機能

  1. 01型付きクラスへの設定バインド
  2. 02Data Annotations による検証
  3. 03複雑なカスタム検証の実装
  4. 04名前付きオプションの設定
  5. 05オプションのライフタイム選択
  6. 06バインド後のオプション変更

活用シーン

  1. 01起動時の必須設定チェック
  2. 02相互依存する設定値の検証
  3. 03複数のデータベース設定の管理
  4. 04バックグラウンドサービスでの設定変更への対応

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill microsoft-extensions-configuration

インストール後、Claude Code で/microsoft-extensions-configurationを入力して読み込まれたか確認します。

詳細

Microsoft.Extensions Configuration パターン

このスキルを使用する場面

次のような場合にこのスキルを使用します。

  • appsettings.json の構成を、型安全なクラスにバインドする
  • アプリケーションの起動時に構成を検証する(フェイルファスト)
  • 複雑な設定の検証ロジックを実装する
  • テストしやすく、保守しやすい構成クラスを設計する
  • IOptions、IOptionsSnapshot、IOptionsMonitor を理解する

構成の検証が重要な理由

問題: 接続文字列の欠落、無効な URL、範囲外の値など、構成ミスが原因でアプリケーションが実行時に失敗することはよくあります。こうした失敗は構成を読み込む場所から遠く離れたビジネスロジックの深部で発生するため、デバッグが困難になります。

解決策: 起動時に構成を検証します。構成が無効な場合、明確なエラーメッセージとともにアプリケーションは直ちに失敗します。これが「フェイルファスト」の原則です。

// 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"

パターン 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;
}
構成からバインドする
// In Program.cs or service registration
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> - singleton, read once at startup
    public EmailService(IOptions<SmtpSettings> options)
    {
        _settings = options.Value;
    }
}

パターン 2:Data Annotations による検証

シンプルな検証ルールには、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;
}
Data Annotations による検証を有効にする
builder.Services.AddOptions<SmtpSettings>()
    .BindConfiguration(SmtpSettings.SectionName)
    .ValidateDataAnnotations()  // Enable attribute-based validation
    .ValidateOnStart();         // Validate immediately at startup

重要なポイント: .ValidateOnStart() は不可欠です。これがないと、オプションが初めてアクセスされたときにのみ検証が実行されます。その時点では、アプリケーションの実行開始から数分、あるいは数時間が経過している可能性があります。


パターン 3:複雑な検証に IValidateOptions を使用する

Data Annotations はシンプルなルールには有効ですが、複雑な検証には IValidateOptions<T> が必要です。

IValidateOptions を使用する場面
シナリオData AnnotationsIValidateOptions
必須フィールド✅✅
範囲チェック✅✅
正規表現パターン✅✅
プロパティ間の検証❌✅
条件付き検証❌✅
外部サービスのチェック❌✅
コンテキスト付きのカスタムエラーメッセージ制限あり✅
バリデーターへの依存性注入❌✅
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;
    }
}
バリデーターを登録する
builder.Services.AddOptions<SmtpSettings>()
    .BindConfiguration(SmtpSettings.SectionName)
    .ValidateDataAnnotations()  // Run attribute validation first
    .ValidateOnStart();

// Register the custom validator
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();

実行順序が重要です: まず Data Annotations が実行され、その後に IValidateOptions のバリデーターが実行されます。すべてのエラーが収集され、まとめて報告されます。


パターン 4:依存関係を持つバリデーター

IValidateOptions のバリデーターは DI から解決されるため、依存関係を持たせることができます。

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;
    }
}

パターン 5:名前付きオプション

同じ設定型のインスタンスが複数ある場合(例:複数のデータベース接続):

// 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");
    }
}
名前付きオプションのバリデーター
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;
    }
}

パターン 6:オプションの有効期間

3 つのオプションインターフェイスについて理解します。

インターフェイス有効期間変更時に再読み込み使用例
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;

        // 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);
        }
    }
}

パターン 7:事後構成

バインド後、検証前にオプションを変更します。

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
builder.Services.AddOptions<ApiSettings>()
    .BindConfiguration("Api")
    .PostConfigure<IHostEnvironment>((options, env) =>
    {
        if (env.IsDevelopment())
        {
            options.Timeout = TimeSpan.FromMinutes(5); // Longer timeout for debugging
        }
    });

パターン 8:完全な例 - 本番環境向け設定クラス

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>();

避けるべきアンチパターン

1. 構成への手動アクセス
// 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. コンストラクターでの検証
// 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. 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. 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;
}

構成バリデーターのテスト

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 = "[email protected]",
            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 = "[email protected]",
            Password = null  // Missing!
        };

        var result = _validator.Validate(null, settings);

        result.Succeeded.Should().BeFalse();
        result.FailureMessage.Should().Contain("Password is required");
    }
}

まとめ

原則実装
フェイルファスト.ValidateOnStart()
型安全POCO クラスにバインドする
シンプルな検証Data Annotations
複雑な検証IValidateOptions<T>
プロパティ間のルールIValidateOptions<T>
環境を考慮IHostEnvironment を注入する
テスト可能バリデーターはプレーンなクラス

コメント