ASP.NET Core のオプション パターンについて調べて、いろいろ分かったことがありましたので、以下に備忘録として書いておくことにします。

ASP.NET Core でのオプションパターンとは何かを簡単に書きますと、appsettings.json などの構成設定を、厳密な型が定義された POCO (Plain Old CLR Object) クラスにバインドし、型安全かつシンプルに扱うためのデザインパターンということです。詳しくは Microsoft のドキュメント「ASP.NET Core のオプション パターン」を見てください。
この記事では、OptionsConfigurationServiceCollectionExtensions.Configure メソッドのオーバーロード Configure<TOptions>(IServiceCollection, IConfiguration) を利用して、appsettings.json のあるセクションから情報を読んで、その情報を POCO に格納し、DI コンテナに登録して ASP.NET Core アプリで利用するケースについて説明します。
(1) appsettings.json
以下の内容とします。この中の SoapService セクションの内容を POCO に取り込みます。
{
・・・中略・・・
"SoapService": {
"SoapScheme": "https",
"SoapServiceHost": "SoapDevService",
"CustomerAccountManager": {
"Endpoint": "{SoapScheme}://{SoapServiceHost}/CustomerAccountManager.asmx",
"Username": "",
"Password": "",
"TimeoutSeconds": 30,
"MaxReceivedMessageSize": 10485760
}
}
}
(2) POCO クラスの定義
POCO のクラス定義は以下の通りとします。パブリックプロパティの名前は appsettings.json の各項目の名前と一致させる必要があります。SoapService フィールドは Microsoft のドキュメントのサンプルコードにならって追加しました。アプリのコード(この記事では Program.cs)で "SoapService" 文字列をハードコーディングしないようにするために追加するとのことです。
namespace RazorPages.Models
{
public class SoapServiceOptions
{
public const string SoapService = "SoapService";
public string SoapScheme { get; set; } = "";
public string SoapServiceHost { get; set; } = "";
public CustomerAccountManagerConfig CustomerAccountManager { get; set; } = new();
}
public class CustomerAccountManagerConfig
{
public string Endpoint { get; set; } = "";
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public int TimeoutSeconds { get; set; } = 0;
public int MaxReceivedMessageSize { get; set; } = 0;
}
}
(3) Configure メソッドを追加
Program.cs に以下のように Configure メソッドを追加します。Configure メソッドは内部で次のことを行います: (1) IConfiguration のセクションを読み取る、(2) Bind() を使って TOptions に値をコピーする、(3) 結果 Options パターン(IOptions<TOptions>, IOptionsSnapshot<TOptions>, IOptionsMonitor<TOptions>)が DI コンテナに登録されます。
namespace RazorPages
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// ・・・中略・・・
// Configure メソッドを追加
builder.Services.Configure<SoapServiceOptions>(
builder.Configuration.GetSection(SoapServiceOptions.SoapService));
// ・・・中略・・・
}
}
}
Options パターン IOptions<TOptions>, IOptionsSnapshot<TOptions>, IOptionsMonitor<TOptions> の違いを簡単に説明すると以下の通りです。
-
IOptions<TOptions>(シングルトン相当)アプリ起動時に設定ファイルを読み込み、常に同じ設定値を保持し続けます。
-
IOptionsSnapshot<TOptions>(スコープ相当)IOptionsSnapshot を使ってDIを受け取ると、HTTPリクエストごとに appsettings.json の変更を検知して最新の値に再生成してくれます。
-
IOptionsMonitor<SoapService>(シングルトン相当+リアルタイム通知)シングルトンでありながら、設定ファイルが書き換わった瞬間に最新の値を反映・通知できる特殊なライフサイクルです。
(4) 一部内容を書き換え
PostConfigure を使って一部の内容を書き換えます。Program.cs で Configure メソッドの後に以下のようにコードを追加します。SoapServiceHost に DateTime.Now.ToString() を設定したのは書き変わるタイミングを知るためです。IOptionsSnapshot と IOptionsMonitor で SoapServiceHost の書き換えのタイミングがどのように異なるかが興味のあるところです。
namespace RazorPages
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// ・・・中略・・・
// Configure メソッドを追加
builder.Services.Configure<SoapServiceOptions>(
builder.Configuration.GetSection(SoapServiceOptions.SoapService));
// 一部内容の書き換え
builder.Services.PostConfigure<SoapServiceOptions>(soap =>
{
soap.SoapServiceHost = DateTime.Now.ToString();
soap.CustomerAccountManager.Endpoint = "/new-endpoint";
});
// ・・・中略・・・
}
}
}
(5) ページ/コントローラーでの情報の取得
ページ/コントローラーでの情報の取得は、ASP.NET Core に備わっている DI 機能を使って、コンストラクタの引数に IOptions<TOptions>, IOptionsSnapshot<TOptions>, IOptionsMonitor<TOptions> を注入してやることで可能です。以下のコードは Razor Pages の例で、この記事の一番上に貼った画像に示すページを表示したものです。
Config.html.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using RazorPages.Models;
namespace RazorPages.Pages
{
public class ConfigModel : PageModel
{
private readonly SoapServiceOptions _soap;
private readonly SoapServiceOptions _soapSnapshot;
private readonly SoapServiceOptions _soapMonitor;
public ConfigModel(IOptions<SoapServiceOptions> soap,
IOptionsSnapshot<SoapServiceOptions> soapSnapshot,
IOptionsMonitor<SoapServiceOptions> soapMonitor)
{
_soap = soap.Value;
_soapSnapshot = soapSnapshot.Value;
_soapMonitor = soapMonitor.CurrentValue;
}
public string SoapScheme => _soap.SoapScheme;
public string SoapServiceHost =>
_soap.SoapServiceHost;
public string SoapEndpoint =>
_soap.CustomerAccountManager.Endpoint;
public string SoapSchemeSnapshot =>
_soapSnapshot.SoapScheme;
public string SoapServiceHostSnapshot =>
_soapSnapshot.SoapServiceHost;
public string SoapEndpointSnapshot =>
_soapSnapshot.CustomerAccountManager.Endpoint;
public string SoapSchemeMonitor =>
_soapMonitor.SoapScheme;
public string SoapServiceHostMonitor =>
_soapMonitor.SoapServiceHost;
public string SoapEndpointMonitor =>
_soapMonitor.CustomerAccountManager.Endpoint;
public void OnGet()
{
}
}
}
Config.html
@page
@model RazorPages.Pages.ConfigModel
@{
ViewData["Title"] = "Config";
}
<h3>IOptions<TOptions></h3>
<ul>
<li>SoapScheme: @Model.SoapScheme</li>
<li>SoapServiceHost: @Model.SoapServiceHost</li>
<li>SoapEndpoint: @Model.SoapEndpoint</li>
</ul>
<h3>IOptionsSnapshot<TOptions></h3>
<ul>
<li>SoapScheme: @Model.SoapSchemeSnapshot</li>
<li>SoapServiceHost: @Model.SoapServiceHostSnapshot</li>
<li>SoapEndpoint: @Model.SoapEndpointSnapshot</li>
</ul>
<h3>IOptionsMonitor<TOptions></h3>
<ul>
<li>SoapScheme: @Model.SoapSchemeMonitor</li>
<li>SoapServiceHost: @Model.SoapServiceHostMonitor</li>
<li>SoapEndpoint: @Model.SoapEndpointMonitor</li>
</ul>
(6) 検証結果の説明
この記事の一番上の画像は検証用に作成した ASP.NET Core Razor Pages アプリの Config ページを表示したものです。赤および青の下線を引いた部分の違いがポイントです。
IOptions<TOptions> はシングルトンですので、アプリを再起動しない限り常に同じ値を保持し続けます。appsettings.json ファイルの SoapScheme を https ⇒ http に書き換えても、Program.cs の PostConfigure メソッドで時刻を設定し直しても、赤および青の下線を引いた部分は変わりません。
IOptionsSnapshot<TOptions> スコープ相当ですので、HTTP 要求ご��に appsettings.json の変更および PostConfigure メソッドでの書き換え検知して最新の値に再生成してくれます。上の画像は appsettings.json ファイルの SoapScheme を https ⇒ http に書き直して保存した後、再度 HTTP リクエストをかけてその応答を示したものです。この後も SoapServiceHost は要求・応答の度にその時刻に変わります。
IOptionsMonitor<TOptions> は基本シングルトンですが、appsettings.json ファイルが書き換わった瞬間に最新の値を反映してくれます。上の画像は appsettings.json ファイルの SoapScheme を https ⇒ http に書き直して保存した後、再度 HTTP リクエストをかけてその応答を示したものです。SoapServiceHost は appsettings.json ファイルが書き換わった時刻になります。その後はシングルトンとして動きますので、appsettings.json ファイルを書き換えない限り何度 HTTP リクエストをかけても値は変わりません。
上記のような Page や Controller で値を受け取るという使い方なら IOption または IOptionSnapshot を使えばよく、IOptionsMonitor の出番は無さそうです。IOptionsMonitor はこのような使い方ではなくて、IOptionsMonitor<TOptions>.OnChange メソッドでリスナーを登録し、設定変更を即時検知してそのリスナーでサーバー側で何か動かす(キャッシュ更新、長寿命サービスの設定差し替え、接続中クライアントへの通知など)のに使うのが適切なようです。
IOptionsMonitor<TOptions>.OnChange メソッドを使った例は別の記事に書く予定です。