mcpField
D
dotnet-csharp-configuration

dotnet-csharp-configuration

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

dotnet-csharp-configuration について

Microsoft.Extensions.ConfigurationとMicrosoft.Extensions.Optionsを使う.NETアプリ開発者向けの設定ガイドです。設定ソースの優先順位、Optionsのバインドと検証、ユーザーシークレット、環境別設定、機能フラグを扱います。

主な機能

  1. 01設定ソースと優先順位の説明
  2. 02強く型付けされたOptionsのバインドと検証
  3. 033種類のOptionsインターフェースのライフタイム比較
  4. 04ユーザーシークレットと環境変数の設定
  5. 05Microsoft.FeatureManagementによる機能フラグ

活用シーン

  1. 01SMTPなどのOptions設定と検証
  2. 02環境別のアプリ設定の上書き
  3. 03機能フラグやフィルターによるエンドポイント制御

インストール

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

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

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

詳細

dotnet-csharp-configuration

Microsoft.Extensions.Configuration と Microsoft.Extensions.Options を使用する .NET アプリケーションの構成パターンです。Options パターン(IOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>)、検証、ユーザーシークレット、環境に基づく構成、Microsoft.FeatureManagement による機能フラグを扱います。

関連項目:[skill:dotnet-csharp-dependency-injection] サービス登録パターン、[skill:dotnet-csharp-coding-standards] 命名規則。


構成ソースと優先順位

WebApplication.CreateBuilder の既定の構成ソース(後に指定したものが優先されます):

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. ユーザーシークレット(Development のみ)
  4. 環境変数
  5. コマンドライン引数
var builder = WebApplication.CreateBuilder(args);
// Sources above are loaded automatically. Add custom sources:
builder.Configuration.AddJsonFile("features.json", optional: true, reloadOnChange: true);

Options パターン

構成セクションを強い型付けのクラスにバインドし、DI 経由で注入します。

Options クラスの定義
public sealed class SmtpOptions
{
    public const string SectionName = "Smtp";

    public string Host { get; set; } = "";
    public int Port { get; set; } = 587;
    public string FromAddress { get; set; } = "";
    public bool UseSsl { get; set; } = true;
}

Options クラスでは、{ get; set; } を使用します(init ではありません)。構成バインダーと PostConfigure がプロパティを変更する必要があるためです。必須フィールドには、データアノテーションを使用して [Required] を指定してください。

登録
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration(SmtpOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();
appsettings.json
{
  "Smtp": {
    "Host": "smtp.example.com",
    "Port": 587,
    "FromAddress": "[email protected]",
    "UseSsl": true
  }
}

Options インターフェイス

インターフェイス有効期間再読み込みの動作用途
IOptions<T>シングルトン起動後は再読み込みされません静的な構成、ほとんどのサービス
IOptionsSnapshot<T>スコープリクエスト/スコープごとに再読み込みされますASP.NET のリクエストごとの構成
IOptionsMonitor<T>シングルトンライブ再読み込み+変更通知シングルトン、バックグラウンドサービス
注入の例
// Static -- most common, singleton-safe
public sealed class EmailService(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;

    public Task SendAsync(string to, string subject, string body,
        CancellationToken ct = default)
    {
        // Use _smtp.Host, _smtp.Port, etc.
        return Task.CompletedTask;
    }
}

// Live reload in singletons -- monitors config file changes
public sealed class FeatureService(IOptionsMonitor<FeatureOptions> monitor)
{
    public bool IsEnabled(string feature)
        => monitor.CurrentValue.EnabledFeatures.Contains(feature);
}

// Per-request in scoped services -- reads latest config each request
public sealed class PricingService(IOptionsSnapshot<PricingOptions> snapshot)
{
    public decimal GetMarkup() => snapshot.Value.MarkupPercent;
}
IOptionsMonitor<T> による変更通知
public sealed class CacheService : IDisposable
{
    private readonly IDisposable? _changeListener;
    private CacheOptions _current;

    public CacheService(IOptionsMonitor<CacheOptions> monitor)
    {
        _current = monitor.CurrentValue;
        _changeListener = monitor.OnChange(updated =>
        {
            _current = updated;
            // React to config change -- flush cache, resize pool, etc.
        });
    }

    public void Dispose() => _changeListener?.Dispose();
}

Options の検証

データアノテーション
using System.ComponentModel.DataAnnotations;

public sealed class SmtpOptions
{
    public const string SectionName = "Smtp";

    [Required, MinLength(1)]
    public string Host { get; set; } = "";

    [Range(1, 65535)]
    public int Port { get; set; } = 587;

    [Required, EmailAddress]
    public string FromAddress { get; set; } = "";
}

builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration(SmtpOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Fail fast at startup, not on first use
IValidateOptions<T>(複雑な検証)

プロパティ間のチェックや外部依存関係が必要な検証ロジックで使用します。

public sealed class SmtpOptionsValidator : IValidateOptions<SmtpOptions>
{
    public ValidateOptionsResult Validate(string? name, SmtpOptions options)
    {
        var failures = new List<string>();

        if (options.UseSsl && options.Port == 25)
        {
            failures.Add("Port 25 does not support SSL. Use 465 or 587.");
        }

        if (string.IsNullOrWhiteSpace(options.Host))
        {
            failures.Add("SMTP host is required.");
        }

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

// Register the validator
builder.Services.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>();
ValidateOnStart(フェイルファスト)

最初に解決するときではなく、起動時に構成エラーを検出するため、常に .ValidateOnStart() を使用してください。指定しない場合、無効な構成で例外が発生するのは、IOptions<T>.Value に初めてアクセスしたときだけです。


ユーザーシークレット(Development)

開発中は、機密性の高い値をソース管理の対象外に保存します。

# Initialize (once per project)
dotnet user-secrets init

# Set values
dotnet user-secrets set "Smtp:Host" "smtp.example.com"
dotnet user-secrets set "ConnectionStrings:Default" "Server=..."

# List all secrets
dotnet user-secrets list

# Clear all
dotnet user-secrets clear

ユーザーシークレットは ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json に保存され、Development では appsettings.json の値を上書きします。

主なルール:

  • 本番環境ではユーザーシークレットを使用しないでください。環境変数、Azure Key Vault、またはその他のボールトプロバイダーを使用してください
  • ASPNETCORE_ENVIRONMENT=Development の場合、ユーザーシークレットは自動的に読み込まれます
  • Web 以外のホストでは、明示的に追加してください:builder.Configuration.AddUserSecrets<Program>()

環境に基づく構成

環境変数
// Hierarchical keys use __ (double underscore) as separator
// Environment variable: Smtp__Host=smtp.prod.com
// Maps to: configuration["Smtp:Host"]
環境ごとのファイル
appsettings.json                 # Base (all environments)
appsettings.Development.json     # Overrides for dev
appsettings.Staging.json         # Overrides for staging
appsettings.Production.json      # Overrides for prod
// Set environment via ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT
// Defaults to "Production" if not set
var env = builder.Environment.EnvironmentName; // "Development", "Staging", "Production"
条件付きサービス登録
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddSingleton<IEmailSender, ConsoleEmailSender>();
}
else
{
    builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
}

Microsoft.FeatureManagement による機能フラグ

Microsoft.FeatureManagement.AspNetCore は、フィルター、ターゲティング、段階的なロールアウトに対応した、体系的な機能フラグ機能を提供します。

セットアップ
dotnet add package Microsoft.FeatureManagement.AspNetCore
builder.Services.AddFeatureManagement();
構成
{
  "FeatureManagement": {
    "NewDashboard": true,
    "BetaSearch": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": { "Value": 50 }
        }
      ]
    },
    "DarkMode": {
      "EnabledFor": [
        {
          "Name": "Targeting",
          "Parameters": {
            "Audience": {
              "Users": [ "[email protected]" ],
              "Groups": [
                { "Name": "Beta", "RolloutPercentage": 100 }
              ],
              "DefaultRolloutPercentage": 0
            }
          }
        }
      ]
    }
  }
}
コードでの使用
// Inject IFeatureManager
public sealed class DashboardController(IFeatureManager featureManager) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> Get(CancellationToken ct = default)
    {
        if (await featureManager.IsEnabledAsync("NewDashboard"))
        {
            return Ok(new { version = "v2", dashboard = "new" });
        }

        return Ok(new { version = "v1", dashboard = "legacy" });
    }
}
Feature Gate 属性
// Entire endpoint gated on feature flag
[FeatureGate("BetaSearch")]
[HttpGet("search")]
public async Task<IActionResult> Search(string query, CancellationToken ct = default)
{
    var results = await _searchService.SearchAsync(query, ct);
    return Ok(results);
}
機能フィルター
フィルター目的
Percentageリクエストの N% に対して有効化(ランダム)
TimeWindow開始日から終了日まで有効化
Targeting特定のユーザー、グループ、またはロールアウト率に対して有効化
カスタムドメイン固有のロジックに対応する IFeatureFilter を実装
カスタム機能フィルター
[FilterAlias("Browser")]
public sealed class BrowserFeatureFilter(IHttpContextAccessor accessor) : IFeatureFilter
{
    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var userAgent = accessor.HttpContext?.Request.Headers.UserAgent.ToString() ?? "";
        var settings = context.Parameters.Get<BrowserFilterSettings>();

        return Task.FromResult(
            settings?.AllowedBrowsers?.Any(b =>
                userAgent.Contains(b, StringComparison.OrdinalIgnoreCase)) ?? false);
    }
}

public sealed class BrowserFilterSettings
{
    public string[] AllowedBrowsers { get; init; } = [];
}

// Register
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<BrowserFeatureFilter>();

名前付き Options

同じ Options 型のインスタンスが複数必要な場合(複数の API クライアントなど)は、名前付き Options を使用します。

// Registration with names
builder.Services
    .AddOptions<ApiClientOptions>("GitHub")
    .BindConfiguration("ApiClients:GitHub");

builder.Services
    .AddOptions<ApiClientOptions>("Jira")
    .BindConfiguration("ApiClients:Jira");

// Resolution via IOptionsSnapshot<T> or IOptionsMonitor<T>
public sealed class ApiClientFactory(IOptionsSnapshot<ApiClientOptions> snapshot)
{
    public HttpClient CreateFor(string name)
    {
        var options = snapshot.Get(name); // "GitHub" or "Jira"
        return new HttpClient { BaseAddress = new Uri(options.BaseUrl) };
    }
}

構成後の処理

すべての構成ソースを処理した後に、既定値または上書きを適用します。

builder.Services.PostConfigure<SmtpOptions>(options =>
{
    // Ensure a default port if none specified
    if (options.Port == 0)
    {
        options.Port = options.UseSsl ? 465 : 25;
    }
});

構成のテスト

[Fact]
public void SmtpOptions_Validates_InvalidPort()
{
    var options = new SmtpOptions
    {
        Host = "smtp.example.com",
        FromAddress = "[email protected]",
        Port = 25,
        UseSsl = true
    };

    var validator = new SmtpOptionsValidator();
    var result = validator.Validate(null, options);

    Assert.True(result.Failed);
    Assert.Contains("Port 25 does not support SSL", result.FailureMessage);
}

[Fact]
public void Configuration_BindsCorrectly()
{
    var config = new ConfigurationBuilder()
        .AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Smtp:Host"] = "smtp.test.com",
            ["Smtp:Port"] = "465",
            ["Smtp:FromAddress"] = "[email protected]",
        })
        .Build();

    var options = new SmtpOptions();
    config.GetSection("Smtp").Bind(options);

    Assert.Equal("smtp.test.com", options.Host);
    Assert.Equal(465, options.Port);
}

参考資料

コメント