.NET でのオプション パターン

オプション パターンでは、クラスを使用して、関連する設定のグループへの、厳密に型指定されたアクセスが提供されます。 構成設定がシナリオ別に個々のクラスに分離されるとき、アプリは次の 2 つの重要なソフトウェア エンジニアリング原則に従います。

構成データを検証するメカニズムもオプションによって提供されます。 詳しくは、「オプションの検証」セクションをご覧ください。

階層的な構成をバインドする

関連する構成値を読み取る方法としては、オプション パターンを使用することをお勧めします。 オプション パターンは、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"に追加することです。 次のコードでは、TransientFaultHandlingOptionsConfigure でサービスコンテナーに追加され、構成にバインドされます:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

前述の例の builderHostApplicationBuilder のインスタンスです。

ヒント

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 を使用して変更の発生を監視し、それに応じて対応します。

オプションのインターフェイス

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<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 回計算され、要求の有効期間中キャッシュされます。 更新された構成値の読み取りをサポートする構成プロバイダーを使用しているとき、構成の変更は、アプリの開始後に読み取られます。

IOptionsMonitorIOptionsSnapshot の違いは次のとおりです。

  • IOptionsMonitor は常に最新のオプション値を取得するIOptionsMonitor です。これは、シングルトンの依存関係で特に便利です。
  • IOptionsSnapshotIOptionsSnapshot であり、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

オプション・モニターを使用するために、オプション・オブジェクトは構成セクションと同じ方法で構成されます。

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:PersonalizeFeatures: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 class sealed 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 は、特定の名前付きインスタンスではなく、すべての名前付きインスタンスを対象とするために使用されます。 ConfigureAllPostConfigureAll では、この規則が使用されます。

OptionsBuilder API

OptionsBuilder<TOptions> は、TOptions インスタンスの構成に使用されます。 OptionsBuilder は名前付きオプションの作成を簡略化します。これは最初の AddOptions<TOptions>(string optionsName) の呼び出しに対する 1 つのパラメーターにすぎず、後続のすべての呼び出しが表示されなくなるためです。 サービスの依存関係を受け入れるオプションの検証と ConfigureOptions のオーバーロードは、OptionsBuilder を介してのみ可能です。

OptionsBuilder は、「オプションの検証」セクションで使用されます。

DI サービスを使用してオプションを構成する

オプションの構成中、次の 2 つの方法で依存関係挿入からサービスにアクセスできます。

  • 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";
});

関連項目