ASP.NET Core のオプション パターン

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の ASP.NET Core 8.0 バージョンを参照してください。

作成者: Rick Anderson

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

  • カプセル化:
    • 構成設定に依存するクラスは、それが使用する構成設定のみに依存する。
  • 懸念事項の分離:
    • アプリのさまざまな部分の設定は、互いに依存していないか、結合されていない。

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

この記事では、ASP.NET Core でのオプションのパターンについて説明します。 コンソール アプリでオプションのパターンを使用する方法の詳細については、「.NET でのオプションのパターン」を参照してください。

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

関連する構成値を読み取る方法としては、オプション パターンを使用することをお勧めします。 たとえば、以下の構成値を読み取るには、次のようにします:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

次の PositionOptions クラスを作成します:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

オプション クラス:

  • 非抽象である必要があります。
  • config の対応する項目がバインドされている型のパブリックの読み取り/書き込みプロパティがあります。
  • 構成内の一致するエントリにバインドされたそれ自体の読み取り/書き込みプロパティがあります。
  • フィールドがバインドされて "いません"。 上のコード Position はバインドされません。 Position フィールドは、クラスを構成プロバイダーにバインドするときに、アプリで文字列 "Position" をハードコーディングする必要をなくすために使用されます。

コード例を次に示します。

  • ConfigurationBinder.Bind を呼び出して、PositionOptions クラスを Position セクションにバインドします。
  • Position 構成データを表示します。
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

ConfigurationBinder.Get<T> 指定された型をバインドして返します。 ConfigurationBinder.Get<T>ConfigurationBinder.Bind を使用するよりも便利な場合があります。 次のコードは、PositionOptions クラスで ConfigurationBinder.Get<T> を使用する方法を示しています:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

Bind では、抽象クラスの具体化も可能です。 抽象クラスである SomethingWithAName を使用する次のコードを検討します。

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

次のコードは、NameTitleOptions 構成の値を表示します。

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

Bind の呼び出しは、Get<> の呼び出しほどは厳密ではありません。

  • Bind では、abstract の具体化が可能です。
  • Get<> ではインスタンス自体を作成する必要があります。

オプション パターン

"オプション パターン" を使用するときのもう 1 つの方法は、Position セクションをバインドし、それを依存関係挿入サービス コンテナーに追加することです。 次のコードでは、PositionOptionsConfigure でサービスコンテナーに追加され、構成にバインドされます:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

下記のコードは、上記のコードを使用して位置オプションを読み取ります:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

上のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は読み取られ "ません"。 アプリの開始後に変更を読み取るには、IOptionsSnapshot を使用します。

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

ポスト構成のシナリオでは、すべての IConfigureOptions<TOptions> 構成が行われた後で、オプションを設定または変更できます。

IOptionsFactory<TOptions> は、新しいオプション インスタンスを作成します。 Create メソッドが 1 つ含まれています。 既定の実装では、登録されている IConfigureOptions<TOptions>IPostConfigureOptions<TOptions> がすべて受け取られ、先にすべての構成を実行し、その後、ポスト構成を実行します。 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions> が区別され、適切なインターフェイスのみが呼び出されます。

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions> によって使用され、TOptions インスタンスをキャッシュします。 IOptionsMonitorCache<TOptions> は、値が再計算されるよう、モニターのオプション インスタンスを無効にします (TryRemove)。 TryAdd を利用し、手動で値を入力できます。 Clear メソッドは、すべての名前付きインスタンスをオンデマンドで再作成するときに使用されます。

IOptionsSnapshot を使用して更新データを読み取る

IOptionsSnapshot<TOptions>の使用

  • オプションは、要求の有効期間中にアクセスされ、キャッシュされたとき、要求につき 1 回計算されます。
  • これは、スコープ付きサービスであり、要求ごとに再計算されるため、パフォーマンスの大幅な低下が発生する可能性があります。 詳細については、この GitHub のイシュー構成バインドのパフォーマンスの向上に関する記事を参照してください。
  • 更新された構成値の読み取りをサポートする構成プロバイダーを使用しているとき、構成の変更は、アプリの開始後に読み取られます。

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

  • IOptionsMonitor は常に最新のオプション値を取得するシングルトン サービスです。これは、シングルトンの依存関係で特に便利です。
  • IOptionsSnapshotスコープ サービスであり、IOptionsSnapshot<T> オブジェクトの構築時にオプションのスナップショットを提供します。 オプションのスナップショットは、一時的な依存関係およびスコープのある依存関係で使用されるように設計されています。

次のコードでは IOptionsSnapshot<TOptions> を使用します。

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更が読み取られます。

IOptionsMonitor

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

IOptionsMonitor<TOptions> の使用例を次に示します。

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

IConfigureNamedOptions を使用した名前付きオプションのサポート

名前付きオプション:

  • 複数の構成セクションが同じプロパティにバインドされている場合に便利です。
  • 大文字と小文字の区別があります。

次の appsettings.json ファイルを考えてみます。

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year をバインドする 2 つのクラスを作成するのではなく、各セクションに対して次のクラスを使用します。

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

次のコードは、名前付きオプションを構成します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

次のコードは、名前付きオプションを表示します。

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

すべてのオプションが名前付きインスタンスです。 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 は、「オプションの検証」セクションで使用されます。

カスタム リポジトリの追加の詳細については、「AddOptions を使用してカスタム リポジトリを構成する」を参照してください。

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

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

  • OptionsBuilder<TOptions>Configure に構成デリゲートを渡します。 OptionsBuilder<TOptions> から Configure のオーバーロードが与えられます。これにより、最大 5 つのサービスを使用してオプションを構成できます。

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> または IConfigureNamedOptions<TOptions> を実装する型を作成し、その型をサービスとして登録します。

サービスの作成は複雑なため、Configure に構成デリゲートを渡す方法をおすすめします。 型を作成することは、Configure を呼び出すときにフレームワークが行うことと同じです。 Configure を呼び出すと、一時的な汎用の IConfigureNamedOptions<TOptions> が登録されます。これには、指定された汎用サービスの型を受け入れるコンストラクターが含まれています。

オプションの検証

オプションの検証により、オプションの値を検証できます。

次の appsettings.json ファイルを考えてみます。

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

次のクラスは "MyConfig" 構成セクションにバインドする目的で使用され、いくつかの DataAnnotations 規則が適用されます。

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

コード例を次に示します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

ValidateDataAnnotations 拡張メソッドは Microsoft.Extensions.Options.DataAnnotations NuGet パッケージに定義されています。 Microsoft.NET.Sdk.Web SDK を使用する Web アプリの場合、このパッケージは共有フレームワークから暗黙的に参照されます。

次のコードは、構成値または検証エラーを表示します。

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

次のコードは、デリゲートを使用して、より複雑な検証規則を適用します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> および IValidatableObject

次のクラスは、IValidateOptions<TOptions> を実装します。

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions は、Program.cs からクラスに検証コードを移動できるようにします。

前のコードを使用すると、次のコードにより Program.cs で検証が有効になります。

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

オプションの検証では IValidatableObject もサポートされています。 クラス自体に含まれるクラスのクラス レベルの検証を実行するには:

  • IValidatableObject インターフェイスとその Validate メソッドをクラス内で実装します。
  • Program.cs 内で ValidateDataAnnotations を呼び出します。

ValidateOnStart

オプションの検証は、IOptions<TOptions>IOptionsSnapshot<TOptions>、または IOptionsMonitor<TOptions> の実装が作成されるときに初めて実行されます。 オプションの検証を能動的に実行するには、アプリの開始時に Program.csValidateOnStart を呼び出します。

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

オプションのポスト構成

ポスト構成を IPostConfigureOptions<TOptions> を使用して設定します。 ポスト構成は、すべての IConfigureOptions<TOptions> 構成が行われた後で実行されます。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure は、名前付きオプションのポスト構成に使用できます。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

すべての構成インスタンスをポスト構成するには、PostConfigureAll を使用します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Program.cs のアクセスオプション

Program.csIOptions<TOptions> または IOptionsMonitor<TOptions> にアクセスするには、WebApplication.ServicesGetRequiredService を呼び出します。

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

その他のリソース

作成者: Kirk Larkin および Rick Anderson

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

  • カプセル化:
    • 構成設定に依存するクラスは、それが使用する構成設定のみに依存する。
  • 懸念事項の分離:
    • アプリのさまざまな部分の設定は、互いに依存していないか、結合されていない。

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

この記事では、ASP.NET Core でのオプションのパターンについて説明します。 コンソール アプリでオプションのパターンを使用する方法の詳細については、「.NET でのオプションのパターン」を参照してください。

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

関連する構成値を読み取る方法としては、オプション パターンを使用することをお勧めします。 たとえば、以下の構成値を読み取るには、次のようにします:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

次の PositionOptions クラスを作成します:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

オプション クラス:

  • パラメーターのないパブリック コンストラクターを持った非抽象でなければなりません。
  • 型のパブリックな読み取り/書き込みプロパティは、すべてバインドされます。
  • フィールドはバインド "されません"。 上のコード Position はバインドされません。 Position フィールドは、クラスを構成プロバイダーにバインドするときに、アプリで文字列 "Position" をハードコーディングする必要をなくすために使用されます。

コード例を次に示します。

  • ConfigurationBinder.Bind を呼び出して、PositionOptions クラスを Position セクションにバインドします。
  • Position 構成データを表示します。
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

ConfigurationBinder.Get<T> 指定された型をバインドして返します。 ConfigurationBinder.Get<T>ConfigurationBinder.Bind を使用するよりも便利な場合があります。 次のコードは、PositionOptions クラスで ConfigurationBinder.Get<T> を使用する方法を示しています:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

"オプション パターン" を使用するときのもう 1 つの方法は、Position セクションをバインドし、それを依存関係挿入サービス コンテナーに追加することです。 次のコードでは、PositionOptionsConfigure でサービスコンテナーに追加され、構成にバインドされます:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

下記のコードは、上記のコードを使用して位置オプションを読み取ります:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

上のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は読み取られ "ません"。 アプリの開始後に変更を読み取るには、IOptionsSnapshot を使用します。

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

ポスト構成のシナリオでは、すべての IConfigureOptions<TOptions> 構成が行われた後で、オプションを設定または変更できます。

IOptionsFactory<TOptions> は、新しいオプション インスタンスを作成します。 Create メソッドが 1 つ含まれています。 既定の実装では、登録されている IConfigureOptions<TOptions>IPostConfigureOptions<TOptions> がすべて受け取られ、先にすべての構成を実行し、その後、ポスト構成を実行します。 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions> が区別され、適切なインターフェイスのみが呼び出されます。

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions> によって使用され、TOptions インスタンスをキャッシュします。 IOptionsMonitorCache<TOptions> は、値が再計算されるよう、モニターのオプション インスタンスを無効にします (TryRemove)。 TryAdd を利用し、手動で値を入力できます。 Clear メソッドは、すべての名前付きインスタンスをオンデマンドで再作成するときに使用されます。

IOptionsSnapshot を使用して更新データを読み取る

IOptionsSnapshot<TOptions>の使用

  • オプションは、要求の有効期間中にアクセスされ、キャッシュされたとき、要求につき 1 回計算されます。
  • これは、スコープ付きサービスであり、要求ごとに再計算されるため、パフォーマンスの大幅な低下が発生する可能性があります。 詳細については、この GitHub のイシュー構成バインドのパフォーマンスの向上に関する記事を参照してください。
  • 更新された構成値の読み取りをサポートする構成プロバイダーを使用しているとき、構成の変更は、アプリの開始後に読み取られます。

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

  • IOptionsMonitor は常に最新のオプション値を取得するシングルトン サービスです。これは、シングルトンの依存関係で特に便利です。
  • IOptionsSnapshotスコープ サービスであり、IOptionsSnapshot<T> オブジェクトの構築時にオプションのスナップショットを提供します。 オプションのスナップショットは、一時的な依存関係およびスコープのある依存関係で使用されるように設計されています。

次のコードでは IOptionsSnapshot<TOptions> を使用します。

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更が読み取られます。

IOptionsMonitor

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

IOptionsMonitor<TOptions> の使用例を次に示します。

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

IConfigureNamedOptions を使用した名前付きオプションのサポート

名前付きオプション:

  • 複数の構成セクションが同じプロパティにバインドされている場合に便利です。
  • 大文字と小文字の区別があります。

次の appsettings.json ファイルを考えてみます。

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year をバインドする 2 つのクラスを作成するのではなく、各セクションに対して次のクラスを使用します。

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

次のコードは、名前付きオプションを構成します。

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

次のコードは、名前付きオプションを表示します。

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

すべてのオプションが名前付きインスタンスです。 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 は、「オプションの検証」セクションで使用されます。

カスタム リポジトリの追加の詳細については、「AddOptions を使用してカスタム リポジトリを構成する」を参照してください。

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

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

  • OptionsBuilder<TOptions>Configure に構成デリゲートを渡します。 OptionsBuilder<TOptions> から Configure のオーバーロードが与えられます。これにより、最大 5 つのサービスを使用してオプションを構成できます。

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> または IConfigureNamedOptions<TOptions> を実装する型を作成し、その型をサービスとして登録します。

サービスの作成は複雑なため、Configure に構成デリゲートを渡す方法をおすすめします。 型を作成することは、Configure を呼び出すときにフレームワークが行うことと同じです。 Configure を呼び出すと、一時的な汎用の IConfigureNamedOptions<TOptions> が登録されます。これには、指定された汎用サービスの型を受け入れるコンストラクターが含まれています。

オプションの検証

オプションの検証により、オプションの値を検証できます。

次の appsettings.json ファイルを考えてみます。

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

次のクラスは "MyConfig" 構成セクションにバインドする目的で使用され、いくつかの DataAnnotations 規則が適用されます。

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

コード例を次に示します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

ValidateDataAnnotations 拡張メソッドは Microsoft.Extensions.Options.DataAnnotations NuGet パッケージに定義されています。 Microsoft.NET.Sdk.Web SDK を使用する Web アプリの場合、このパッケージは共有フレームワークから暗黙的に参照されます。

次のコードは、構成値または検証エラーを表示します。

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

次のコードは、デリゲートを使用して、より複雑な検証規則を適用します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> および IValidatableObject

次のクラスは、IValidateOptions<TOptions> を実装します。

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions は、Program.cs からクラスに検証コードを移動できるようにします。

前のコードを使用すると、次のコードにより Program.cs で検証が有効になります。

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

オプションの検証では IValidatableObject もサポートされています。 クラス自体に含まれるクラスのクラス レベルの検証を実行するには:

  • IValidatableObject インターフェイスとその Validate メソッドをクラス内で実装します。
  • Program.cs 内で ValidateDataAnnotations を呼び出します。

ValidateOnStart

オプションの検証は、IOptions<TOptions>IOptionsSnapshot<TOptions>、または IOptionsMonitor<TOptions> の実装が作成されるときに初めて実行されます。 オプションの検証を能動的に実行するには、アプリの開始時に Program.csValidateOnStart を呼び出します。

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

オプションのポスト構成

ポスト構成を IPostConfigureOptions<TOptions> を使用して設定します。 ポスト構成は、すべての IConfigureOptions<TOptions> 構成が行われた後で実行されます。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure は、名前付きオプションのポスト構成に使用できます。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

すべての構成インスタンスをポスト構成するには、PostConfigureAll を使用します。

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Program.cs のアクセスオプション

Program.csIOptions<TOptions> または IOptionsMonitor<TOptions> にアクセスするには、WebApplication.ServicesGetRequiredService を呼び出します。

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

その他のリソース

作成者: Kirk Larkin および Rick Anderson

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

  • カプセル化:
    • 構成設定に依存するクラスは、それが使用する構成設定のみに依存する。
  • 懸念事項の分離:
    • アプリのさまざまな部分の設定は、互いに依存していないか、結合されていない。

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

このトピックでは、ASP.NET Core でのオプションのパターンについて説明します。 コンソール アプリでオプションのパターンを使用する方法の詳細については、「.NET でのオプションのパターン」を参照してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

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

関連する構成値を読み取る方法としては、オプション パターンを使用することをお勧めします。 たとえば、以下の構成値を読み取るには、次のようにします:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

次の PositionOptions クラスを作成します:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

オプション クラス:

  • パラメーターのないパブリック コンストラクターを持った非抽象でなければなりません。
  • 型のパブリックな読み取り/書き込みプロパティは、すべてバインドされます。
  • フィールドはバインド "されません"。 上のコード Position はバインドされません。 Position プロパティは、クラスを構成プロバイダーにバインドするときに、アプリで文字列 "Position" をハードコーディングする必要をなくすために使用されます。

コード例を次に示します。

  • ConfigurationBinder.Bind を呼び出して、PositionOptions クラスを Position セクションにバインドします。
  • Position 構成データを表示します。
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

ConfigurationBinder.Get<T> 指定された型をバインドして返します。 ConfigurationBinder.Get<T>ConfigurationBinder.Bind を使用するよりも便利な場合があります。 次のコードは、PositionOptions クラスで ConfigurationBinder.Get<T> を使用する方法を示しています:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

"オプション パターン" を使用するときのもう 1 つの方法は、Position セクションをバインドし、それを依存関係挿入サービス コンテナーに追加することです。 次のコードでは、PositionOptionsConfigure でサービスコンテナーに追加され、構成にバインドされます:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

下記のコードは、上記のコードを使用して位置オプションを読み取ります:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

上のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は読み取られ "ません"。 アプリの開始後に変更を読み取るには、IOptionsSnapshot を使用します。

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

ポスト構成のシナリオでは、すべての IConfigureOptions<TOptions> 構成が行われた後で、オプションを設定または変更できます。

IOptionsFactory<TOptions> は、新しいオプション インスタンスを作成します。 Create メソッドが 1 つ含まれています。 既定の実装では、登録されている IConfigureOptions<TOptions>IPostConfigureOptions<TOptions> がすべて受け取られ、先にすべての構成を実行し、その後、ポスト構成を実行します。 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions> が区別され、適切なインターフェイスのみが呼び出されます。

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions> によって使用され、TOptions インスタンスをキャッシュします。 IOptionsMonitorCache<TOptions> は、値が再計算されるよう、モニターのオプション インスタンスを無効にします (TryRemove)。 TryAdd を利用し、手動で値を入力できます。 Clear メソッドは、すべての名前付きインスタンスをオンデマンドで再作成するときに使用されます。

IOptionsSnapshot を使用して更新データを読み取る

IOptionsSnapshot<TOptions> を使用すると、オプションは要求の有効期間中にアクセスされ、キャッシュされたとき、要求につき 1 回計算されます。 更新された構成値の読み取りをサポートする構成プロバイダーを使用しているとき、構成の変更は、アプリの開始後に読み取られます。

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

  • IOptionsMonitor は常に最新のオプション値を取得するシングルトン サービスです。これは、シングルトンの依存関係で特に便利です。
  • IOptionsSnapshotスコープ サービスであり、IOptionsSnapshot<T> オブジェクトの構築時にオプションのスナップショットを提供します。 オプションのスナップショットは、一時的な依存関係およびスコープのある依存関係で使用されるように設計されています。

次のコードでは IOptionsSnapshot<TOptions> を使用します。

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更が読み取られます。

IOptionsMonitor

次のコードは、MyOptions のバインド先となる構成インスタンスを登録します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

IOptionsMonitor<TOptions> の使用例を次に示します。

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

上記のコードでは、アプリが開始された後の JSON 構成ファイルへの変更は既定で読み取られます。

IConfigureNamedOptions を使用した名前付きオプションのサポート

名前付きオプション:

  • 複数の構成セクションが同じプロパティにバインドされている場合に便利です。
  • 大文字と小文字の区別があります。

次の appsettings.json ファイルを考えてみます。

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year をバインドする 2 つのクラスを作成するのではなく、各セクションに対して次のクラスを使用します。

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

次のコードは、名前付きオプションを構成します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

次のコードは、名前付きオプションを表示します。

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

すべてのオプションが名前付きインスタンスです。 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 は、「オプションの検証」セクションで使用されます。

カスタム リポジトリの追加の詳細については、「AddOptions を使用してカスタム リポジトリを構成する」を参照してください。

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

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

  • OptionsBuilder<TOptions>Configure に構成デリゲートを渡します。 OptionsBuilder<TOptions> から Configure のオーバーロードが与えられます。これにより、最大 5 つのサービスを使用してオプションを構成できます。

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> または IConfigureNamedOptions<TOptions> を実装する型を作成し、その型をサービスとして登録します。

サービスの作成は複雑なため、Configure に構成デリゲートを渡す方法をおすすめします。 型を作成することは、Configure を呼び出すときにフレームワークが行うことと同じです。 Configure を呼び出すと、一時的な汎用の IConfigureNamedOptions<TOptions> が登録されます。これには、指定された汎用サービスの型を受け入れるコンストラクターが含まれています。

オプションの検証

オプションの検証により、オプションの値を検証できます。

次の appsettings.json ファイルを考えてみます。

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

次のクラスは、"MyConfig" 構成セクションにバインドされ、いくつかの DataAnnotations 規則を適用します。

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

コード例を次に示します。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

ValidateDataAnnotations 拡張メソッドは Microsoft.Extensions.Options.DataAnnotations NuGet パッケージに定義されています。 Microsoft.NET.Sdk.Web SDK を使用する Web アプリの場合、このパッケージは共有フレームワークから暗黙的に参照されます。

次のコードは、構成値または検証エラーを表示します。

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

次のコードは、デリゲートを使用して、より複雑な検証規則を適用します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

複雑な検証用の IValidateOptions

次のクラスは、IValidateOptions<TOptions> を実装します。

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions は、StartUp からクラスに検証コードを移動できるようにします。

前のコードを使用すると、次のコードにより Startup.ConfigureServices で検証が有効になります。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

オプションのポスト構成

ポスト構成を IPostConfigureOptions<TOptions> を使用して設定します。 ポスト構成は、すべての IConfigureOptions<TOptions> 構成が行われた後で実行されます。

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure は、名前付きオプションのポスト構成に使用できます。

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

すべての構成インスタンスをポスト構成するには、PostConfigureAll を使用します。

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

スタートアップ時にオプションにアクセスする

サービスは Configure メソッドの実行前に構築されるため、IOptions<TOptions> および IOptionsMonitor<TOptions>Startup.Configure で使用できます。

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

Startup.ConfigureServices では IOptions<TOptions> または IOptionsMonitor<TOptions> は使用しないでください。 サービスの登録順序が原因で、オプションの状態が一貫しない場合があります。

Options.ConfigurationExtensions NuGet パッケージ

ASP.NET Core アプリでは、Microsoft.Extensions.Options.ConfigurationExtensions パッケージが暗黙的に参照されます。