WebSurfer's Home

Filter by APML

ASP.NET Core のオプション パターン (その2)

by WebSurfer 15. October 2026 16:36

先の記事「ASP.NET Core のオプション パターン」で "IOptionsMonitor は、IOptionsMonitor<TOptions>.OnChange メソッドでリスナーを登録し、設定変更を即時検知してそのリスナーでサーバー側で何か動かす(キャッシュ更新、長寿命サービスの設定差し替え、接続中クライアントへの通知など)のに使うのが適切なようです" と書きました。この記事ではその一例を書きます。

検証用に作成した Config2 ページ

上の画像は検証用に作成した Config2 ページをブラウザに表示したものです。上に書いた "設定変更を即時検知してそのリスナーでサーバー側で何か動かす" と言うのをキャッシュを削除するようにしてみました。

Config2 ページおよび OnChange メソッドを実装してキャッシュを削除する Watcher クラスのコードはこの記事の最後の方に載せます。

上の画像の赤枠の部分は DI コンテナに登録された IOptionsMonitor<TOptions> から取得したデータで、SoapScheme は appsettings.json に設定してある値 "https"、SoapServiceHost は Program.cs で PostConfigure メソッドを使って DateTime.Now.ToString() に書き換えたものです。IOptionsMonitor<TOptions> は基本シングルトンとして DI コンテナに登録されるので、何度要求・応答を繰り返しても同じ値になります。

上の画像の青枠の部分は ASP.NET Core のキャッシュから取得したものです。Config2 ページの OnGet メソッドでキャッシュに値があるかを調べ、無ければその時点での日時をキャッシュに設定、あればキャッシュから値を取得して表示するようにしています。キャッシュを削除しない限り、何度要求・応答を繰り返しても同じ日時が表示されます。

青枠の上の Current Time は要求を受けた時点での日時を DateTime.Now.ToString() で取得して表示しています。従って要求・応答を繰り返すとそのたびにその時点での日時が表示されます。

上記の動作を確認した後、そのまま Web アプリは動いている状態で、Visual Studio で appsettings.json ファイルを開いてその中の項目 SoapScheme の値を https から http に変更してセーブします。

DI コンテナに登録した IOptionsMonitor<TOptions> は appsettings.json を変更してセーブした時点で最新の情報に書き換えられます。今回の例では、SoapScheme が https から http に書き変わり、SoapServiceHost は appsettings.json を変更してセーブした日時に書き換えられます。

その後で再度 Config2 ページを要求し、その応答を表示したものが下の画像です。画像の赤の下線を引いた部分を見てください。SoapScheme の値が https から http に、SoapServiceHost は appsettings.json ファイルを変更してセーブした時点の日時に変更されています。

appsettings.json を書換後の Config2 ページ

さらに、IOptionsMonitor<TOptions>.OnChange メソッドのリスナーにはキャッシュを削除するコードを設定しましたので、appsettings.json をセーブした時点でリスナーが起動され、キャッシュが削除されます。

上に書いたように、Config2 ページの OnGet メソッドでは、キャッシュに値があるかを調べ、無ければその時点での日時をキャッシュに設定して表示するようにしています。

したがって、appsettings.json ファイルを変更してセーブした後で Config2 ページを要求すると、キャッシュは削除されているので、要求した時点での日時をキャッシュに設定した上で、それを表示するようにしています、上の画像の青の下線で示した Cached Time 部分がそれです。

SoapServiceHost が 11:43:48 で、Cached Time が 11:44:00 と違う(後者が遅い)のは、SoapServiceHost が appsettings.json を変更してセーブした時刻、Cached Time は Config2 を要求した時刻になるからです。

以下に検証に用いたコードを載せておきます。

まず、先の記事「ASP.NET Core のオプション パターン」に書いた、(1) appsettings.json、(2) POCO クラスの定義、(3) Configure メソッドを追加、(4) 一部内容を書き換え と同じコードを実装します。

(5) Watcher クラス

IOptionsMonitor<TOptions>.OnChange メソッドを実装した Watcher クラスを定義します。IOptionsMonitor<TOptions> オブジェクトとキャッシュに用いる IMemoryCache オブジェクトをコンストラクタ経由の DI により取得します。

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using RazorPages.Models;

namespace RazorPages.Services
{
    public class SoapConfigWatcher : IDisposable
    {
        private SoapServiceOptions _current;
        private readonly IMemoryCache _memoryCache;
        private readonly IDisposable? _sub;
        private readonly ILogger<SoapConfigWatcher> _logger;

        public SoapConfigWatcher(IOptionsMonitor<SoapServiceOptions> monitor,
                                 IMemoryCache memoryCache,
                                 ILogger<SoapConfigWatcher> logger)
        {
            _memoryCache = memoryCache;
            _logger = logger;
            _current = monitor.CurrentValue;
            _sub = monitor.OnChange(cfg => { 
                _current = cfg;
                // キャッシュ更新、長寿命サービスの設定差し替え、接続中ク
                // ライアントへの通知など。以下はキャッシュを削除する例:
                _memoryCache.Remove(CacheKeys.Entry);  
            });
            if (_sub == null)
            {
                _logger.LogCritical("OnChange returned null.");
                throw new InvalidOperationException("OnChange returned null.");
            }
        }
        public SoapServiceOptions Current => _current;
        public void Dispose() => _sub!.Dispose();
    }
}

(6) Watcher クラスを DI コンテナに登録

Program.cs に以下のコードを追加し、Watcher を DI コンテナに登録します。

namespace RazorPages
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // ・・・中略・・・

            // Watcher を DI コンテナに登録
            builder.Services.AddSingleton<SoapConfigWatcher>();

            // ・・・中略・・・
        }
    }
}

(7) Config2 ページの実装

Config2.html.cs

Watcher のオブジェクトとキャッシュに用いる IMemoryCache オブジェクトをコンストラクタ経由の DI により取得します。

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Caching.Memory;
using RazorPages.Services;

namespace RazorPages.Pages
{
    public class Config2Model : PageModel
    {
        private readonly SoapConfigWatcher _watcher;
        private readonly IMemoryCache _memoryCache;

        public Config2Model(SoapConfigWatcher watcher, 
                            IMemoryCache memoryCache)
        {
            _watcher = watcher;
            _memoryCache = memoryCache;
        }

        public string SoapScheme => _watcher.Current.SoapScheme;
        public string SoapServiceHost => _watcher.Current.SoapServiceHost;
        public DateTime CachedDateTime { get; set; }

        public void OnGet()
        {
            // 次のコードでは、TryGetValue を使用して、キャッシュに
            // 値があるかを調べ、あればキャッシュから値を取得。無け
            // れば DateTime.Now をキャッシュに設定
            DateTime cacheEntry;

            if (!_memoryCache.TryGetValue(CacheKeys.Entry, out cacheEntry))
            {
                cacheEntry = DateTime.Now;
                _memoryCache.Set(CacheKeys.Entry, cacheEntry);
            }

            CachedDateTime = cacheEntry;
        }
    }
}

Config2.html

@page
@model RazorPages.Pages.Config2Model

@{
    ViewData["Title"] = "Config2";
}

<h1>Config2</h1>

<p>Current SoapService Configuration:</p>
<ul>
    <li>SoapScheme: @Model.SoapScheme</li>
    <li>SoapServiceHost: @Model.SoapServiceHost</li>
</ul>

<h3>Current Time: @DateTime.Now.ToString()</h3>
<h3>Cached Time: @Model.CachedDateTime.ToString()</h3>

Tags: , , ,

CORE

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

by WebSurfer 28. September 2026 15:45

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

検証用に作成した Config ページ

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> の違いを簡単に説明すると以下の通りです。

  1. IOptions<TOptions>(シングルトン相当)アプリ起動時に設定ファイルを読み込み、常に同じ設定値を保持し続けます。
  2. IOptionsSnapshot<TOptions>(スコープ相当)IOptionsSnapshot を使ってDIを受け取ると、HTTPリクエストごとに appsettings.json の変更を検知して最新の値に再生成してくれます。
  3. IOptionsMonitor<TOptions>(シングルトン相当+リアルタイム通知)シングルトンでありながら、設定ファイルが書き換わった瞬間に最新の値を反映・通知できる特殊なライフサイクルです。

(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 メソッドを使った例は別の記事「ASP.NET Core のオプション パターン (その2)」に書きました。

Tags: , , , ,

CORE

MVC5 で IHttpClientFactory を DI で利用

by WebSurfer 27. January 2025 14:26

下の図の構成で、ASP.NET MVC5 において Dependency Injection (DI) により IHttpClientFactory を取得し、それから HttpClient を生成して Web API からデータを取得し、そのデータをブラウザに表示する方法を書きます。

MVC5 で IHttpClientFactory を利用

ASP.NET Core MVC の例は先の記事「ASP.NET と HttpClient (CORE)」に書きました���

ASP.NET Core の場合は、Visual Studio のテンプレートを使ってプロジェクトを作成すればデフォルトで DI 機能が実装されるので、Program.cs で AddHttpClient メソッドを使って IHttpClientFactory を IServiceCollection (DI コンテナ) に登録するだけで、Controller はコンストラクタの引数経由で IHttpClientFactory を取得できます。

一方、ターゲットフレームワークが .NET Framework 場合、Visual Studio のテンプレートで作成した MVC5 アプリには DI 機能は実装されません。なので、最初の課題はそれにどのように DI 機能を追加するかになります。

ちなみに DI を利用して IHttpClientFactory を取得するのは必須です。何故かと言うと、Microsoft のドキュメント「IHttpClientFactory の代替手段」に書いてありますように、以下のことを回避できるというメリットがあるからです。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題

ターゲットフレームワークが .NET Framework のアプリでも、バージョンが 4.6.2 以降であれば ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を利用して DI 機能を実装できます。

.NET Framework 4.8 の MVC5 アプリに DI 機能を実装する詳しい方法は、先の記事「MVC5 での Dependency Injection」に書きました。この記事では、その記事のアプリの IServiceCollection (DI コンテナ) に IHttpClientFactory を登録し、それを Controller でどのように使って Web API に要求を出し、データを取得するかを書きます。

(1) Microsoft.Extensions.Http

NuGet パッケージ Microsoft.Extensions.Http をインストールします。これは AddHttpClient メソッドを使用して IHttpClientFactory を IServiceCollection(DI コンテナ)に登録できるようにするため必要です。

Microsoft.Extensions.Http

(2) AddHttpClient メソッドの追加

先の記事「MVC5 での Dependency Injection」に書いた Global.asax.cs のコード内の ConfigureServices メソッドに、以下のように AddHttpClient メソッドを追加します。この一行で Controller はコンストラクタの引数経由で IHttpClientFactory を受け取れるようになります。

// DI コンテナにサービスを登録するメソッド
private void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ScopedThing>();
    services.AddTransient<HomeController>();

    // IHttpClientFactory を DI して利用できるよう追加
    services.AddHttpClient();
}

(3) Controller

Controller の例です。ブラウザが Hero アクションメソッドを呼び出すと、DI 機能により IHttpClientFactory が Controller のコンストラクタの引数経由で渡されます。

IHttpClientFactory から生成した HttpClient を使って Web API に要求を出して JSON 形式のデータを取得し、それを .NET の List<Hero> 型のオブジェクトにデシリアライズして View に渡しています。

using Mvc5DependencyInjection.Models;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Text.Json;

namespace Mvc5DependencyInjection.Controllers
{
    public class HomeController : Controller
    {
        private readonly ScopedThing _scopedThing;

        // IHttpClientFactory を DI して利用できるよう追加
        private readonly IHttpClientFactory _clientFactory;

        // コンストラクタの引数経由 IHttpClientFactory が DI される
        public HomeController(ScopedThing scopedThing, 
                              IHttpClientFactory clientFactory)
        {
            this._scopedThing = scopedThing;
            this._clientFactory = clientFactory;
        }

        // ・・・中略・・・

        public async Task<ActionResult> Hero()
        {
            // IHttpClientFactory から HttpClient を取得
            HttpClient client = _clientFactory.CreateClient();

            var url = "Web API の url";
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            HttpResponseMessage response = await client.SendAsync(request);
            List<Hero> list = null;

            if (response.IsSuccessStatusCode)
            {
                using (Stream responseStream =
                              await response.Content.ReadAsStreamAsync())
                {
                    list = await JsonSerializer.
                           DeserializeAsync<List<Hero>>(responseStream);
                }
            }

            return View(list);
        }
    }
}

(4) Model

namespace Mvc5DependencyInjection.Models
{
    public class Hero
    {
        public int id { get; set; }
        public string name { get; set; }
    }
}

(5) View

@model IEnumerable<Mvc5DependencyInjection.Models.Hero>

@{
    ViewBag.Title = "Hero";
}

<h2>Hero</h2>

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.id)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.name)
        </th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.id)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.name)
            </td>
        </tr>
    }

</table>

(6) 実行結果

上のコードを実行するとブラウザには以下のように表示されます。

実行結果

Tags: , , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  October 2026  >>
MoTuWeThFrSaSu
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar