ASP.NET Core で変更トークンを使用して変更を検出する

"変更トークン" は、状態の変更を追跡するために使用される汎用の低レベル構成ブロックです。

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

IChangeToken インターフェイス

IChangeToken により、変更が発生したという通知が伝達されます。 IChangeTokenMicrosoft.Extensions.Primitives 名前空間内にあります。 Microsoft.Extensions.Primitives NuGet パッケージは、ASP.NET Core アプリに暗黙的に提供されます。

IChangeToken に次の 2 つのプロパティがあります。

  • ActiveChangeCallbacks は、トークンによってコールバックがプロアクティブに生成されるかどうかを示します。 ActiveChangedCallbacksfalse に設定されている場合、コールバックは呼び出されず、アプリは変更のために HasChanged をポーリングする必要があります。 変更がまったく発生しない場合、または基になる変更リスナーが破棄されるか無効になっている場合、トークンがまったくキャンセルされない可能性もあります。
  • HasChanged は、変更が発生したかどうかを示す値を受け取ります。

IChangeToken インターフェイスには、RegisterChangeCallback(Action<Object>, Object) というメソッドが含まれています。これを使うと、トークンが変更されたときに呼び出されるコールバックを登録できます。 コールバックが呼び出される前に HasChanged を設定する必要があります。

ChangeToken クラス

ChangeToken は、変更が発生したことの通知を伝達するために使用される静的クラスです。 ChangeTokenMicrosoft.Extensions.Primitives 名前空間内にあります。 Microsoft.Extensions.Primitives NuGet パッケージは、ASP.NET Core アプリに暗黙的に提供されます。

ChangeToken.OnChange(Func<IChangeToken>, Action) メソッドを使うと、トークンが変更されるたびに呼び出される Action を登録できます。

  • Func<IChangeToken> はトークンを生成します。
  • Action は、トークンが変更されたときに呼び出されます。

ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) オーバーロードには、トークン コンシューマー Action に渡される追加の TState パラメーターを指定できます。

OnChange では IDisposable が返されます。 Dispose を呼び出すと、トークンによるその後の変更のリッスンが停止され、トークンのリソースが解放されます。

ASP.NET Core での変更のトークンの使用例

変更トークンは、オブジェクトの変更を監視するために ASP.NET Core の主要な領域で使用されます。

  • ファイルへの変更を監視するために、IFileProviderWatch メソッドでは、指定された監視対象のファイルまたはフォルダーの IChangeToken が作成されます。
  • IChangeToken トークンをキャッシュ エントリに追加して、変更時にキャッシュの削除をトリガーすることができます。
  • TOptions の変更のために、OptionsMonitor<TOptions> の既定の実装である IOptionsMonitor<TOptions> には、1 つ以上の IOptionsChangeTokenSource<TOptions> インスタンスを受け取るオーバーロードが含まれています。 各インスタンスは、オプションの変更を追跡するために変更通知のコールバックを登録する IChangeToken を返します。

構成の変更の監視

既定では、ASP.NET Core テンプレートは、JSON 構成ファイル (appsettings.jsonappsettings.Development.jsonappsettings.Production.json) を使用して、アプリの構成設定を読み込みます。

これらのファイルは、reloadOnChange パラメーターを受け取る、ConfigurationBuilder 上の AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) 拡張メソッドを使用して構成されます。 reloadOnChange は、ファイルの変更時に構成を再読み込みするかどうかを示します。 この設定は、Host の便利なメソッド CreateDefaultBuilder 内に現れます。

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, 
          reloadOnChange: true);

ファイル ベースの構成は、FileConfigurationSource によって表されます。 FileConfigurationSource ではファイルを監視するために IFileProvider が使用されます。

既定では、IFileMonitorPhysicalFileProvider によって提供されます。これは、FileSystemWatcher を使用して、構成ファイルの変更を監視します。

サンプル アプリは、構成の変更を監視するための 2 つの実装を示します。 appsettings ファイルのいずれかが変更されると、ファイルを監視する両方の実装によってカスタム コードが実行されます。サンプル アプリによりコンソールにメッセージが書き込まれます。

構成ファイルの FileSystemWatcher は、1 つの構成ファイルの変更に対して複数のトークンのコールバックをトリガーできます。 複数のトークンのコールバックがトリガーされたときにカスタム コードが一度だけ実行されるようにするために、サンプルの実装ではファイル ハッシュがチェックされます。 サンプルでは、SHA1 ファイル ハッシュが使用されています。 再試行は、指数バックオフを使用して実装されます。

Utilities/Utilities.cs:

public static byte[] ComputeHash(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fs = File.OpenRead(filePath))
                {
                    return System.Security.Cryptography.SHA1
                        .Create().ComputeHash(fs);
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return new byte[20];
}

シンプルなスタートアップ変更トークン

構成再読み込みトークンへの変更通知のために、トークン コンシューマー Action のコールバックを登録します。

Startup.Configureの場合:

ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);

config.GetReloadToken() はトークンを提供します。 コールバックは、InvokeChanged メソッドです。

private void InvokeChanged(IWebHostEnvironment env)
{
    byte[] appsettingsHash = ComputeHash("appSettings.json");
    byte[] appsettingsEnvHash = 
        ComputeHash($"appSettings.{env.EnvironmentName}.json");

    if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
        !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
    {
        _appsettingsHash = appsettingsHash;
        _appsettingsEnvHash = appsettingsEnvHash;

        WriteConsole("Configuration changed (Simple Startup Change Token)");
    }
}

コールバックの state は、IWebHostEnvironment を渡すためのに使われます。これは監視する適切な appsettings 構成ファイルを指定するのに便利です (たとえば、開発環境の場合は appsettings.Development.json)。 ファイル ハッシュは、構成ファイルが 1 回のみ変更されたときの複数のトークンのコールバックのために WriteConsole ステートメントが複数回実行されるのを防ぐために使用されます。

このシステムは、アプリが実行されている限り実行され、ユーザーが無効にすることはできません。

サービスとしての構成の変更の監視

サンプルは次のものを実装します。

  • 基本的なスタートアップ トークンの監視
  • サービスとしての監視
  • 監視を有効および無効にするメカニズム。

サンプルでは IConfigurationMonitor インターフェイスが設定されています。

Extensions/ConfigurationMonitor.cs:

public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}

実装済みのクラスのコンストラクター ConfigurationMonitor は、変更通知のコールバックを登録します。

public ConfigurationMonitor(IConfiguration config, IWebHostEnvironment env)
{
    _env = env;

    ChangeToken.OnChange<IConfigurationMonitor>(
        () => config.GetReloadToken(),
        InvokeChanged,
        this);
}

public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";

config.GetReloadToken() はトークンを提供します。 InvokeChanged はコールバック メソッドです。 このインスタンスの state は、監視の状態にアクセスするために使用される IConfigurationMonitor インスタンスへの参照です。 次の 2 つのプロパティが使用されます。

  • MonitoringEnabled: コールバックでそのカスタム コードを実行する必要があるかどうかを示します。
  • CurrentState: UI で使用するための現在の監視状態を説明します。

InvokeChanged メソッドは、次の点を除けば、前の方法に似ています。

  • MonitoringEnabledtrue ではない限りコードを実行しません。
  • 現在の state をその WriteConsole 出力に出力します。
private void InvokeChanged(IConfigurationMonitor state)
{
    if (MonitoringEnabled)
    {
        byte[] appsettingsHash = ComputeHash("appSettings.json");
        byte[] appsettingsEnvHash = 
            ComputeHash($"appSettings.{_env.EnvironmentName}.json");

        if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
            !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
        {
            string message = $"State updated at {DateTime.Now}";
          

            _appsettingsHash = appsettingsHash;
            _appsettingsEnvHash = appsettingsEnvHash;

            WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
                $"{message}, state:{state.CurrentState}");
        }
    }
}

インスタンス ConfigurationMonitor は、Startup.ConfigureServices でサービスとして登録されます。

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

インデックス ページは、ユーザーに構成監視の制御を提供します。 IConfigurationMonitor のインスタンスは、IndexModel に挿入されます。

Pages/Index.cshtml.cs:

public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}

構成監視 (_monitor) は、監視を有効または無効にし、UI フィードバックの現在の状態を設定するために使用されます。

public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}

OnPostStartMonitoring がトリガーされると、監視が有効になり、現在の状態がクリアされます。 OnPostStopMonitoring がトリガーされると、監視が無効になり、監視が発生していないことを反映するように状態が設定されます。

UI のボタンを使って監視を有効および無効にできます。

Pages/Index.cshtml:

<button class="btn btn-success" asp-page-handler="StartMonitoring">
    Start Monitoring
</button>

<button class="btn btn-danger" asp-page-handler="StopMonitoring">
    Stop Monitoring
</button>

キャッシュされたファイルの変更の監視

IMemoryCache を使用して、ファイルの内容をメモリ内にキャッシュすることができます。 メモリ内キャッシュの説明については、「メモリ内キャッシュ」トピックを参照してください。 以下に示す実装など、追加の手順を実行しないと、ソース データが変更された場合に期限切れの (古い) データがキャッシュから返されます。

たとえば、スライド式有効期限期間の更新時にキャッシュされたソース ファイルの状態を考慮しないと、キャッシュされたファイル データが古くなります。 データの各要求が、スライド式有効期限を更新しますが、ファイルがキャッシュに再読み込みされることはありません。 ファイルのキャッシュされた内容を使用するアプリの機能は、古い内容を受け取る可能性があります。

ファイル キャッシュのシナリオで変更トークンを使用すると、キャッシュに古いファイルの内容が残ることを防止できます。 サンプル アプリは、このアプローチの実装を示しています。

このサンプルでは、GetFileContent を使用して次の操作を実行します。

  • ファイルの内容を返します。
  • 指数バックオフを使用する再試行アルゴリズムを実装し、ファイル アクセスの問題によりファイルのコンテンツの読み取りが一時的に妨げられるケースに対処します。

Utilities/Utilities.cs:

public async static Task<string> GetFileContent(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fileStreamReader = File.OpenText(filePath))
                {
                    return await fileStreamReader.ReadToEndAsync();
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return null;
}

FileService はキャッシュされたファイルの参照の処理するために作成されます。 サービスの GetFileContent メソッド呼び出しは、メモリ内キャッシュからファイルの内容を取得して、呼び出し元 (Services/FileService.cs) に戻そうとします。

キャッシュ キーを使用してキャッシュされた内容が見つからない場合、次の操作が実行されます。

  1. GetFileContent を使用してファイルの内容が取得されます。
  2. 変更トークンは、IFileProviders.Watch を使用してファイル プロバイダーから取得します。 ファイルが変更されたときに、トークンのコールバックがトリガーされます。
  3. ファイルの内容は、スライド式有効期限の期間キャシュされます。 ファイルがキャッシュされている間に変更された場合、キャッシュ エントリを削除するために、MemoryCacheEntryExtensions.AddExpirationToken を使用して変更トークンがアタッチされます。

次の例では、ファイルはアプリのコンテンツ ルートに格納されます。 IWebHostEnvironment.ContentRootFileProvider は、アプリの IWebHostEnvironment.ContentRootPath を指す IFileProvider を取得するために使用されます。 IFileInfo.PhysicalPath を使って filePath を取得します。

public class FileService
{
    private readonly IMemoryCache _cache;
    private readonly IFileProvider _fileProvider;
    private List<string> _tokens = new List<string>();

    public FileService(IMemoryCache cache, IWebHostEnvironment env)
    {
        _cache = cache;
        _fileProvider = env.ContentRootFileProvider;
    }

    public async Task<string> GetFileContents(string fileName)
    {
        var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
        string fileContent;

        // Try to obtain the file contents from the cache.
        if (_cache.TryGetValue(filePath, out fileContent))
        {
            return fileContent;
        }

        // The cache doesn't have the entry, so obtain the file 
        // contents from the file itself.
        fileContent = await GetFileContent(filePath);

        if (fileContent != null)
        {
            // Obtain a change token from the file provider whose
            // callback is triggered when the file is modified.
            var changeToken = _fileProvider.Watch(fileName);

            // Configure the cache entry options for a five minute
            // sliding expiration and use the change token to
            // expire the file in the cache if the file is
            // modified.
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5))
                .AddExpirationToken(changeToken);

            // Put the file contents into the cache.
            _cache.Set(filePath, fileContent, cacheEntryOptions);

            return fileContent;
        }

        return string.Empty;
    }
}

FileService はメモリ キャッシュ サービスと共にサービス コンテナーに登録されます。

Startup.ConfigureServicesの場合:

services.AddMemoryCache();
services.AddSingleton<FileService>();

ページ モデルでは、このサービスを使ってファイルの内容が読み込まれます。

Index ページの OnGet メソッド内 (Pages/Index.cshtml.cs):

var fileContent = await _fileService.GetFileContents("poem.txt");

CompositeChangeToken クラス

1 つのオブジェクト内で 1 つまたは複数の IChangeToken インスタンスを表すためには、CompositeChangeToken クラスを使用します。

var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        {
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });

複合トークンの HasChanged は、いずれかの表現されているトークン HasChangedtrue である場合に、true を報告します。 複合トークンの ActiveChangeCallbacks は、いずれかの表現されているトークン ActiveChangeCallbackstrue である場合に、true を報告します。 複数の同時変更イベントが発生すると、複合変更コールバックが 1 回呼び出されます。

"変更トークン" は、状態の変更を追跡するために使用される汎用の低レベル構成ブロックです。

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

IChangeToken インターフェイス

IChangeToken により、変更が発生したという通知が伝達されます。 IChangeTokenMicrosoft.Extensions.Primitives 名前空間内にあります。 Microsoft.AspNetCore.App メタパッケージを使用しないアプリの場合は、Microsoft.Extensions.Primitives NuGet パッケージのパッケージ参照を作成します。

IChangeToken に次の 2 つのプロパティがあります。

  • ActiveChangeCallbacks は、トークンによってコールバックがプロアクティブに生成されるかどうかを示します。 ActiveChangedCallbacksfalse に設定されている場合、コールバックは呼び出されず、アプリは変更のために HasChanged をポーリングする必要があります。 変更がまったく発生しない場合、または基になる変更リスナーが破棄されるか無効になっている場合、トークンがまったくキャンセルされない可能性もあります。
  • HasChanged は、変更が発生したかどうかを示す値を受け取ります。

IChangeToken インターフェイスには、RegisterChangeCallback(Action<Object>, Object) というメソッドが含まれています。これを使うと、トークンが変更されたときに呼び出されるコールバックを登録できます。 コールバックが呼び出される前に HasChanged を設定する必要があります。

ChangeToken クラス

ChangeToken は、変更が発生したことの通知を伝達するために使用される静的クラスです。 ChangeTokenMicrosoft.Extensions.Primitives 名前空間内にあります。 Microsoft.AspNetCore.App メタパッケージを使用しないアプリの場合は、Microsoft.Extensions.Primitives NuGet パッケージのパッケージ参照を作成します。

ChangeToken.OnChange(Func<IChangeToken>, Action) メソッドを使うと、トークンが変更されるたびに呼び出される Action を登録できます。

  • Func<IChangeToken> はトークンを生成します。
  • Action は、トークンが変更されたときに呼び出されます。

ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) オーバーロードには、トークン コンシューマー Action に渡される追加の TState パラメーターを指定できます。

OnChange では IDisposable が返されます。 Dispose を呼び出すと、トークンによるその後の変更のリッスンが停止され、トークンのリソースが解放されます。

ASP.NET Core での変更のトークンの使用例

変更トークンは、オブジェクトの変更を監視するために ASP.NET Core の主要な領域で使用されます。

  • ファイルへの変更を監視するために、IFileProviderWatch メソッドでは、指定された監視対象のファイルまたはフォルダーの IChangeToken が作成されます。
  • IChangeToken トークンをキャッシュ エントリに追加して、変更時にキャッシュの削除をトリガーすることができます。
  • TOptions の変更のために、OptionsMonitor<TOptions> の既定の実装である IOptionsMonitor<TOptions> には、1 つ以上の IOptionsChangeTokenSource<TOptions> インスタンスを受け取るオーバーロードが含まれています。 各インスタンスは、オプションの変更を追跡するために変更通知のコールバックを登録する IChangeToken を返します。

構成の変更の監視

既定では、ASP.NET Core テンプレートは、JSON 構成ファイル (appsettings.jsonappsettings.Development.jsonappsettings.Production.json) を使用して、アプリの構成設定を読み込みます。

これらのファイルは、reloadOnChange パラメーターを受け取る、ConfigurationBuilder 上の AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) 拡張メソッドを使用して構成されます。 reloadOnChange は、ファイルの変更時に構成を再読み込みするかどうかを示します。 この設定は、WebHost の便利なメソッド CreateDefaultBuilder 内に現れます。

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, 
          reloadOnChange: true);

ファイル ベースの構成は、FileConfigurationSource によって表されます。 FileConfigurationSource ではファイルを監視するために IFileProvider が使用されます。

既定では、IFileMonitorPhysicalFileProvider によって提供されます。これは、FileSystemWatcher を使用して、構成ファイルの変更を監視します。

サンプル アプリは、構成の変更を監視するための 2 つの実装を示します。 appsettings ファイルのいずれかが変更されると、ファイルを監視する両方の実装によってカスタム コードが実行されます。サンプル アプリによりコンソールにメッセージが書き込まれます。

構成ファイルの FileSystemWatcher は、1 つの構成ファイルの変更に対して複数のトークンのコールバックをトリガーできます。 複数のトークンのコールバックがトリガーされたときにカスタム コードが一度だけ実行されるようにするために、サンプルの実装ではファイル ハッシュがチェックされます。 サンプルでは、SHA1 ファイル ハッシュが使用されています。 再試行は、指数バックオフを使用して実装されます。

Utilities/Utilities.cs:

public static byte[] ComputeHash(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fs = File.OpenRead(filePath))
                {
                    return System.Security.Cryptography.SHA1
                        .Create().ComputeHash(fs);
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3)
            {
                throw;
            }

            Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
            runCount++;
        }
    }

    return new byte[20];
}

シンプルなスタートアップ変更トークン

構成再読み込みトークンへの変更通知のために、トークン コンシューマー Action のコールバックを登録します。

Startup.Configureの場合:

ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);

config.GetReloadToken() はトークンを提供します。 コールバックは、InvokeChanged メソッドです。

private void InvokeChanged(IHostingEnvironment env)
{
    byte[] appsettingsHash = ComputeHash("appSettings.json");
    byte[] appsettingsEnvHash = 
        ComputeHash($"appSettings.{env.EnvironmentName}.json");

    if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
        !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
    {
        _appsettingsHash = appsettingsHash;
        _appsettingsEnvHash = appsettingsEnvHash;

        WriteConsole("Configuration changed (Simple Startup Change Token)");
    }
}

コールバックの state は、IHostingEnvironment を渡すためのに使われます。これは監視する適切な appsettings 構成ファイルを指定するのに便利です (たとえば、開発環境の場合は appsettings.Development.json)。 ファイル ハッシュは、構成ファイルが 1 回のみ変更されたときの複数のトークンのコールバックのために WriteConsole ステートメントが複数回実行されるのを防ぐために使用されます。

このシステムは、アプリが実行されている限り実行され、ユーザーが無効にすることはできません。

サービスとしての構成の変更の監視

サンプルは次のものを実装します。

  • 基本的なスタートアップ トークンの監視
  • サービスとしての監視
  • 監視を有効および無効にするメカニズム。

サンプルでは IConfigurationMonitor インターフェイスが設定されています。

Extensions/ConfigurationMonitor.cs:

public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}

実装済みのクラスのコンストラクター ConfigurationMonitor は、変更通知のコールバックを登録します。

public ConfigurationMonitor(IConfiguration config, IHostingEnvironment env)
{
    _env = env;

    ChangeToken.OnChange<IConfigurationMonitor>(
        () => config.GetReloadToken(),
        InvokeChanged,
        this);
}

public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";

config.GetReloadToken() はトークンを提供します。 InvokeChanged はコールバック メソッドです。 このインスタンスの state は、監視の状態にアクセスするために使用される IConfigurationMonitor インスタンスへの参照です。 次の 2 つのプロパティが使用されます。

  • MonitoringEnabled: コールバックでそのカスタム コードを実行する必要があるかどうかを示します。
  • CurrentState: UI で使用するための現在の監視状態を説明します。

InvokeChanged メソッドは、次の点を除けば、前の方法に似ています。

  • MonitoringEnabledtrue ではない限りコードを実行しません。
  • 現在の state をその WriteConsole 出力に出力します。
private void InvokeChanged(IConfigurationMonitor state)
{
    if (MonitoringEnabled)
    {
        byte[] appsettingsHash = ComputeHash("appSettings.json");
        byte[] appsettingsEnvHash = 
            ComputeHash($"appSettings.{_env.EnvironmentName}.json");

        if (!_appsettingsHash.SequenceEqual(appsettingsHash) || 
            !_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
        {
            string message = $"State updated at {DateTime.Now}";
          

            _appsettingsHash = appsettingsHash;
            _appsettingsEnvHash = appsettingsEnvHash;

            WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
                $"{message}, state:{state.CurrentState}");
        }
    }
}

インスタンス ConfigurationMonitor は、Startup.ConfigureServices でサービスとして登録されます。

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

インデックス ページは、ユーザーに構成監視の制御を提供します。 IConfigurationMonitor のインスタンスは、IndexModel に挿入されます。

Pages/Index.cshtml.cs:

public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}

構成監視 (_monitor) は、監視を有効または無効にし、UI フィードバックの現在の状態を設定するために使用されます。

public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}

OnPostStartMonitoring がトリガーされると、監視が有効になり、現在の状態がクリアされます。 OnPostStopMonitoring がトリガーされると、監視が無効になり、監視が発生していないことを反映するように状態が設定されます。

UI のボタンを使って監視を有効および無効にできます。

Pages/Index.cshtml:

<button class="btn btn-success" asp-page-handler="StartMonitoring">
    Start Monitoring
</button>

<button class="btn btn-danger" asp-page-handler="StopMonitoring">
    Stop Monitoring
</button>

キャッシュされたファイルの変更の監視

IMemoryCache を使用して、ファイルの内容をメモリ内にキャッシュすることができます。 メモリ内キャッシュの説明については、「メモリ内キャッシュ」トピックを参照してください。 以下に示す実装など、追加の手順を実行しないと、ソース データが変更された場合に期限切れの (古い) データがキャッシュから返されます。

たとえば、スライド式有効期限期間の更新時にキャッシュされたソース ファイルの状態を考慮しないと、キャッシュされたファイル データが古くなります。 データの各要求が、スライド式有効期限を更新しますが、ファイルがキャッシュに再読み込みされることはありません。 ファイルのキャッシュされた内容を使用するアプリの機能は、古い内容を受け取る可能性があります。

ファイル キャッシュのシナリオで変更トークンを使用すると、キャッシュに古いファイルの内容が残ることを防止できます。 サンプル アプリは、このアプローチの実装を示しています。

このサンプルでは、GetFileContent を使用して次の操作を実行します。

  • ファイルの内容を返します。
  • 指数バックオフを使用する再試行アルゴリズムを実装し、ファイル アクセスの問題によりファイルのコンテンツの読み取りが一時的に妨げられるケースに対処します。

Utilities/Utilities.cs:

public async static Task<string> GetFileContent(string filePath)
{
    var runCount = 1;

    while(runCount < 4)
    {
        try
        {
            if (File.Exists(filePath))
            {
                using (var fileStreamReader = File.OpenText(filePath))
                {
                    return await fileStreamReader.ReadToEndAsync();
                }
            }
            else 
            {
                throw new FileNotFoundException();
            }
        }
        catch (IOException ex)
        {
            if (runCount == 3 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
                runCount++;
            }
        }
    }

    return null;
}

FileService はキャッシュされたファイルの参照の処理するために作成されます。 サービスの GetFileContent メソッド呼び出しは、メモリ内キャッシュからファイルの内容を取得して、呼び出し元 (Services/FileService.cs) に戻そうとします。

キャッシュ キーを使用してキャッシュされた内容が見つからない場合、次の操作が実行されます。

  1. GetFileContent を使用してファイルの内容が取得されます。
  2. 変更トークンは、IFileProviders.Watch を使用してファイル プロバイダーから取得します。 ファイルが変更されたときに、トークンのコールバックがトリガーされます。
  3. ファイルの内容は、スライド式有効期限の期間キャシュされます。 ファイルがキャッシュされている間に変更された場合、キャッシュ エントリを削除するために、MemoryCacheEntryExtensions.AddExpirationToken を使用して変更トークンがアタッチされます。

次の例では、ファイルはアプリのコンテンツ ルートに格納されます。 アプリの ContentRootPath を指す IFileProvider を取得するために、IHostingEnvironment.ContentRootFileProvider が使われます。 IFileInfo.PhysicalPath を使って filePath を取得します。

public class FileService
{
    private readonly IMemoryCache _cache;
    private readonly IFileProvider _fileProvider;
    private List<string> _tokens = new List<string>();

    public FileService(IMemoryCache cache, IHostingEnvironment env)
    {
        _cache = cache;
        _fileProvider = env.ContentRootFileProvider;
    }

    public async Task<string> GetFileContents(string fileName)
    {
        var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
        string fileContent;

        // Try to obtain the file contents from the cache.
        if (_cache.TryGetValue(filePath, out fileContent))
        {
            return fileContent;
        }

        // The cache doesn't have the entry, so obtain the file 
        // contents from the file itself.
        fileContent = await GetFileContent(filePath);

        if (fileContent != null)
        {
            // Obtain a change token from the file provider whose
            // callback is triggered when the file is modified.
            var changeToken = _fileProvider.Watch(fileName);

            // Configure the cache entry options for a five minute
            // sliding expiration and use the change token to
            // expire the file in the cache if the file is
            // modified.
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(5))
                .AddExpirationToken(changeToken);

            // Put the file contents into the cache.
            _cache.Set(filePath, fileContent, cacheEntryOptions);

            return fileContent;
        }

        return string.Empty;
    }
}

FileService はメモリ キャッシュ サービスと共にサービス コンテナーに登録されます。

Startup.ConfigureServicesの場合:

services.AddMemoryCache();
services.AddSingleton<FileService>();

ページ モデルでは、このサービスを使ってファイルの内容が読み込まれます。

Index ページの OnGet メソッド内 (Pages/Index.cshtml.cs):

var fileContent = await _fileService.GetFileContents("poem.txt");

CompositeChangeToken クラス

1 つのオブジェクト内で 1 つまたは複数の IChangeToken インスタンスを表すためには、CompositeChangeToken クラスを使用します。

var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        {
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });

複合トークンの HasChanged は、いずれかの表現されているトークン HasChangedtrue である場合に、true を報告します。 複合トークンの ActiveChangeCallbacks は、いずれかの表現されているトークン ActiveChangeCallbackstrue である場合に、true を報告します。 複数の同時変更イベントが発生すると、複合変更コールバックが 1 回呼び出されます。

その他の技術情報