Erkennen von Änderungen mit Änderungstoken in ASP.NET Core

Ein Änderungstoken ist ein universeller Baustein auf niedriger Ebene, mit dem Änderungen nachverfolgt werden können.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

IChangeToken-Schnittstelle

IChangeToken verbreitet Benachrichtigungen, wenn eine Änderung aufgetreten ist. IChangeToken ist im Microsoft.Extensions.Primitives-Namespace enthalten. Das Microsoft.Extensions.Primitives-NuGet-Paket wird implizit für die ASP.NET Core-Apps bereitgestellt.

IChangeToken verfügt über zwei Eigenschaften:

  • ActiveChangeCallbacks geben an, ob das Token proaktiv Rückrufe auslöst. Wenn ActiveChangedCallbacks auf false festgelegt ist, wird ein Rückruf nie aufgerufen, und die Anwendung muss HasChanged für Änderungen abrufen. Es ist auch möglich, dass ein Token nie abgebrochen wird, wenn keine Änderungen auftreten oder der zugrunde liegenden Änderungslistener gelöscht oder deaktiviert wird.
  • HasChanged ruft einen Wert auf, der angibt, ob eine Änderung aufgetreten ist.

Die IChangeToken-Schnittstelle umfasst die Methode RegisterChangeCallback(Action<Object>, Object). Diese registriert einen Rückruf, der bei einer Tokenänderung aufgerufen wird. HasChanged muss festgelegt werden, bevor der Rückruf aufgerufen wird.

ChangeToken-Klasse

ChangeToken ist eine statische Klasse, die zur Verbreitung von Änderungsbenachrichtigungen verwendet wird. ChangeToken ist im Microsoft.Extensions.Primitives-Namespace enthalten. Das Microsoft.Extensions.Primitives-NuGet-Paket wird implizit für die ASP.NET Core-Apps bereitgestellt.

Die Methode ChangeToken.OnChange(Func<IChangeToken>, Action) registriert eine Action, die bei jeder Änderung des Tokens aufgerufen wird:

  • Func<IChangeToken> erzeugt das Token.
  • Action wird aufgerufen, wenn das Token geändert wird.

Die Überladung ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) akzeptiert einen zusätzlichen TState-Parameter, der an den Tokenconsumer Action übergeben wird.

IDisposable gibt OnChange zurück. Der Aufruf von Dispose beendet den Überwachungsvorgang des Tokens auf weitere Änderungen und gibt die Tokenressourcen frei.

Beispielverwendungen von Änderungstoken in ASP.NET Core

Änderungstoken werden in wichtigen Bereichen von ASP.NET Core verwendet, um auf Objektänderungen zu überwachen:

  • Für die Überwachung von Änderungen an Dateien erstellt die Watch-Methode von IFileProvider ein IChangeToken für die angegebenen Dateien oder einen Ordner, der überwacht werden soll.
  • IChangeToken-Token können zu Cacheeinträgen hinzugefügt werden, um bei Änderungen Cacheentfernungen auszulösen.
  • Für TOptions-Änderungen umfasst die OptionsMonitor<TOptions>-Standardimplementierung von IOptionsMonitor<TOptions> eine Überladung, die eine oder mehrere IOptionsChangeTokenSource<TOptions>-Instanzen akzeptiert. Jede Instanz gibt ein IChangeToken zurück, um den Rückruf einer Änderungsbenachrichtigung zur Nachverfolgung von Optionsänderungen zu registrieren.

Überwachen von Konfigurationsänderungen

ASP.NET Core-Vorlagen verwenden standardmäßig JSON-Konfigurationsdateien (appsettings.json, appsettings.Development.json und appsettings.Production.json), um App-Konfigurationseinstellungen zu laden.

Diese Dateien werden mithilfe der AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean)-Erweiterungsmethode für ConfigurationBuilder konfiguriert, die einen reloadOnChange-Parameter akzeptiert. reloadOnChange gibt an, ob Konfigurationen auf Dateiänderungen neu geladen werden soll. Diese Einstellung wird in der Host-Hilfsmethode CreateDefaultBuilder verwendet:

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

Die dateibasierte Konfiguration wird durch FileConfigurationSource repräsentiert. FileConfigurationSource verwendet IFileProvider zum Überwachen von Dateien.

IFileMonitor wird standardmäßig von einem PhysicalFileProvider bereitgestellt, der zum Überwachen auf Konfigurationsdateiänderungen FileSystemWatcher verwendet.

Die Beispielanwendung veranschaulicht zwei Implementierungen für die Überwachung von Konfigurationsänderungen. Wenn sich eine beliebige der appsettings-Dateien ändert, führen beide Dateiüberwachungsimplementierungen benutzerdefinierten Code aus – die Beispiel-App gibt eine Meldung an die Konsole aus.

Der FileSystemWatcher einer Konfigurationsdatei kann mehrere Tokenrückrufe für eine einzelne Dateikonfigurationsänderung auslösen. Um sicherzustellen, dass der benutzerdefinierte Code bei Auslösung mehrerer Tokenrückrufe nur einmal ausgeführt wird, überprüft die Beispielimplementierung Dateihashes. Das Beispiel verwendet SHA1-Dateihashing. Eine Wiederholung wird mit einem exponentiellen Backoff implementiert.

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];
}

Einfaches Starten von Änderungstoken

Registrieren Sie einen Action-Rückruf eines Tokenconsumers für Änderungsbenachrichtigungen an das Token zum Neuladen der Konfiguration.

In Startup.Configure:

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

config.GetReloadToken() stellt das Token bereit. Der Rückruf ist die InvokeChanged-Methode:

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

Der state des Rückrufs wird zum Übergeben von IWebHostEnvironment verwendet. Dies ist hilfreich, um die richtige appsettings-Konfigurationsdatei anzugeben, die überwacht werden soll (in der Entwicklungsumgebung beispielsweise appsettings.Development.json). Dateihashes werden verwendet, um zu verhindern, dass die WriteConsole-Anweisung mehrere Male ausgeführt wird. Dies liegt an mehreren Tokenrückrufen, wenn die Konfigurationsdatei nur einmal geändert wurde.

Dieses System wird so lange ausgeführt, wie die Anwendung ausgeführt wird. Es kann nicht vom Benutzer deaktiviert werden.

Überwachen von Konfigurationsänderungen als Dienst

Das Beispiel implementiert:

  • Grundlegendes Überwachen des Starttokens.
  • Überwachung als Dienst.
  • Ein Mechanismus zum Aktivieren und Deaktivieren des Monitoring.

In diesem Beispiel wird eine IConfigurationMonitor-Schnittstelle eingerichtet.

Extensions/ConfigurationMonitor.cs:

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

Der Konstruktor der implementierten ConfigurationMonitor-Klasse registriert einen Rückruf für Änderungsbenachrichtigungen:

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() stellt das Token bereit. InvokeChanged ist die Rückrufmethode. In dieser Instanz stellt state einen Verweis auf die IConfigurationMonitor-Instanz dar, die zum Zugriff auf den Überwachungsstatus verwendet wird. Es werden zwei Eigenschaften verwendet:

  • MonitoringEnabled: Diese Eigenschaft gibt an, ob der Rückruf den zugehörigen benutzerdefinierten Code ausführen soll.
  • CurrentState: Diese Eigenschaft beschreibt den aktuellen Überwachungsstatus zur Verwendung in der Benutzeroberfläche.

Die InvokeChanged-Methode ist vergleichbar mit dem früheren Ansatz, außer dass sie:

  • Den Code nicht ausführt, es sei denn, MonitoringEnabled ist true.
  • Den aktuellen state in der zugehörigen WriteConsole-Ausgabe berücksichtigt.
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}");
        }
    }
}

Eine ConfigurationMonitor-Instanz wird in Startup.ConfigureServices als Dienst registriert:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

Die Indexseite bietet das Benutzer Kontrolle über die Konfigurationsüberwachung. Die IConfigurationMonitor-Instanz wird in das IndexModel eingefügt.

Pages/Index.cshtml.cs:

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

Der Konfigurationsmonitor (_monitor) wird verwendet, um die Überwachung zu aktivieren oder zu deaktivieren und den aktuellen Status für Benutzeroberflächenfeedback festzulegen:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Wenn OnPostStartMonitoring ausgelöst wird, wird die Überwachung aktiviert, und der aktuelle Status ist deaktiviert. Wenn OnPostStopMonitoring ausgelöst wird, wird Überwachung deaktiviert, und der Zustand zeigt, dass die Überwachung nicht ausgeführt wird.

Die Überwachung wird über Schaltflächen in der Benutzeroberfläche aktiviert und deaktiviert.

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>

Überwachen von zwischengespeicherten Dateiänderungen

Dateiinhalt kann mithilfe von IMemoryCache speicherintern zwischengespeichert werden. Zwischenspeicherung im Arbeitsspeicher wird im Thema Cache in-memory (Zwischenspeicherung im Arbeitsspeicher) beschrieben. Ohne zusätzliche Schritte, wie der unten beschriebenen Implementierung, werden veraltete Daten aus einem Zwischenspeicher zurückgegeben, wenn sich die Quelldaten ändern.

Wird beispielsweise der Status einer zwischengespeicherten Quelldatei beim Erneuern eines variablen Ablaufzeitraums nicht berücksichtigt, führt dies zu veralteten Dateidaten im Cache. Jede Datenanforderung erneuert den variablen Ablaufzeitraum, aber die Datei wird nie neu in den Zwischenspeicher geladen. Alle Anwendungsfeatures, die den zwischengespeicherten Inhalt der Datei verwenden, erhalten möglicherweise veraltete Inhalte.

Durch die Verwendung von Änderungstoken in einem Szenario mit Dateizwischenspeicherung wird verhindert, dass veraltete Dateiinhalte im Zwischenspeicher auftreten. Die Beispielanwendung zeigt eine Implementierung des Ansatzes.

Das Beispiel verwendet GetFileContent, um:

  • Dateiinhalt zurückzugeben.
  • Implementieren eines Wiederholungsalgorithmus mit exponentiellem Backoff, um Fälle abzudecken, in denen ein Dateizugriffsproblem das Lesen des Dateiinhalts vorübergehend verzögert.

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;
}

Ein FileService wird erstellt, um die zwischengespeicherten Dateisuchvorgänge zu behandeln. Der Aufruf der GetFileContent-Methode des Diensts versucht, den Dateiinhalt aus dem speicherinternen Cache abzurufen und ihn an den Aufrufer zurückzugeben (Services/FileService.cs).

Wenn mithilfe des Cacheschlüssels zwischengespeicherte Inhalte nicht gefunden werden, werden folgende Aktionen durchgeführt:

  1. Der Inhalt der Datei wird mit GetFileContent abgerufen.
  2. Ein Änderungstoken wird mithilfe von IFileProviders.Watch aus dem Dateianbieter abgerufen. Wenn die Datei geändert wird, wird ein Rückruf des Tokens ausgelöst.
  3. Der Inhalt der Datei wird mit einem variablen Ablaufzeitraum zwischengespeichert. Das Änderungstoken wird mit MemoryCacheEntryExtensions.AddExpirationToken angefügt, um den Cacheeintrag zu entfernen, wenn die sich Datei ändert, während sie zwischengespeichert wird.

Im folgenden Beispiel werden Dateien im Inhaltsstammverzeichnis der App gespeichert. IWebHostEnvironment.ContentRootFileProvider wird verwendet, um einen IFileProvider zu erhalten, der auf den IWebHostEnvironment.ContentRootPath der App verweist. Der filePath wird mit IFileInfo.PhysicalPath abgerufen.

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;
    }
}

Der FileService ist zusammen mit dem Speichercachedienst im Dienstcontainer registriert:

In Startup.ConfigureServices:

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

Das Seitenmodell lädt mithilfe des Diensts den Inhalt der Datei.

In der OnGet-Methode auf der Indexseite (Pages/Index.cshtml.cs):

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

CompositeChangeToken-Klasse

Verwenden Sie für die Darstellung von einer oder mehreren IChangeToken-Instanzen in einem einzelnen Objekt die Klasse 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 für die kombinierten Tokenberichte ist true, wenn ein dargestelltes HasChanged-Token true ist. ActiveChangeCallbacks für die kombinierten Tokenberichte ist true, wenn ein dargestelltes ActiveChangeCallbacks-Token true ist. Wenn mehrere Änderungsereignisse gleichzeitig auftreten, wird der Rückruf für die kombinierte Änderung einmal aufgerufen.

Ein Änderungstoken ist ein universeller Baustein auf niedriger Ebene, mit dem Änderungen nachverfolgt werden können.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

IChangeToken-Schnittstelle

IChangeToken verbreitet Benachrichtigungen, wenn eine Änderung aufgetreten ist. IChangeToken ist im Microsoft.Extensions.Primitives-Namespace enthalten. Für Anwendungen, die das Metapaket Microsoft.AspNetCore.App nicht verwenden, erstellen Sie einen Paketverweis auf das NuGet-Paket Microsoft.Extensions.Primitives.

IChangeToken verfügt über zwei Eigenschaften:

  • ActiveChangeCallbacks geben an, ob das Token proaktiv Rückrufe auslöst. Wenn ActiveChangedCallbacks auf false festgelegt ist, wird ein Rückruf nie aufgerufen, und die Anwendung muss HasChanged für Änderungen abrufen. Es ist auch möglich, dass ein Token nie abgebrochen wird, wenn keine Änderungen auftreten oder der zugrunde liegenden Änderungslistener gelöscht oder deaktiviert wird.
  • HasChanged ruft einen Wert auf, der angibt, ob eine Änderung aufgetreten ist.

Die IChangeToken-Schnittstelle umfasst die Methode RegisterChangeCallback(Action<Object>, Object). Diese registriert einen Rückruf, der bei einer Tokenänderung aufgerufen wird. HasChanged muss festgelegt werden, bevor der Rückruf aufgerufen wird.

ChangeToken-Klasse

ChangeToken ist eine statische Klasse, die zur Verbreitung von Änderungsbenachrichtigungen verwendet wird. ChangeToken ist im Microsoft.Extensions.Primitives-Namespace enthalten. Für Anwendungen, die das Metapaket Microsoft.AspNetCore.App nicht verwenden, erstellen Sie einen Paketverweis auf das NuGet-Paket Microsoft.Extensions.Primitives.

Die Methode ChangeToken.OnChange(Func<IChangeToken>, Action) registriert eine Action, die bei jeder Änderung des Tokens aufgerufen wird:

  • Func<IChangeToken> erzeugt das Token.
  • Action wird aufgerufen, wenn das Token geändert wird.

Die Überladung ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) akzeptiert einen zusätzlichen TState-Parameter, der an den Tokenconsumer Action übergeben wird.

IDisposable gibt OnChange zurück. Der Aufruf von Dispose beendet den Überwachungsvorgang des Tokens auf weitere Änderungen und gibt die Tokenressourcen frei.

Beispielverwendungen von Änderungstoken in ASP.NET Core

Änderungstoken werden in wichtigen Bereichen von ASP.NET Core verwendet, um auf Objektänderungen zu überwachen:

  • Für die Überwachung von Änderungen an Dateien erstellt die Watch-Methode von IFileProvider ein IChangeToken für die angegebenen Dateien oder einen Ordner, der überwacht werden soll.
  • IChangeToken-Token können zu Cacheeinträgen hinzugefügt werden, um bei Änderungen Cacheentfernungen auszulösen.
  • Für TOptions-Änderungen umfasst die OptionsMonitor<TOptions>-Standardimplementierung von IOptionsMonitor<TOptions> eine Überladung, die eine oder mehrere IOptionsChangeTokenSource<TOptions>-Instanzen akzeptiert. Jede Instanz gibt ein IChangeToken zurück, um den Rückruf einer Änderungsbenachrichtigung zur Nachverfolgung von Optionsänderungen zu registrieren.

Überwachen von Konfigurationsänderungen

ASP.NET Core-Vorlagen verwenden standardmäßig JSON-Konfigurationsdateien (appsettings.json, appsettings.Development.json und appsettings.Production.json), um App-Konfigurationseinstellungen zu laden.

Diese Dateien werden mithilfe der AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean)-Erweiterungsmethode für ConfigurationBuilder konfiguriert, die einen reloadOnChange-Parameter akzeptiert. reloadOnChange gibt an, ob Konfigurationen auf Dateiänderungen neu geladen werden soll. Diese Einstellung wird in der WebHost-Hilfsmethode CreateDefaultBuilder verwendet:

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

Die dateibasierte Konfiguration wird durch FileConfigurationSource repräsentiert. FileConfigurationSource verwendet IFileProvider zum Überwachen von Dateien.

IFileMonitor wird standardmäßig von einem PhysicalFileProvider bereitgestellt, der zum Überwachen auf Konfigurationsdateiänderungen FileSystemWatcher verwendet.

Die Beispielanwendung veranschaulicht zwei Implementierungen für die Überwachung von Konfigurationsänderungen. Wenn sich eine beliebige der appsettings-Dateien ändert, führen beide Dateiüberwachungsimplementierungen benutzerdefinierten Code aus – die Beispiel-App gibt eine Meldung an die Konsole aus.

Der FileSystemWatcher einer Konfigurationsdatei kann mehrere Tokenrückrufe für eine einzelne Dateikonfigurationsänderung auslösen. Um sicherzustellen, dass der benutzerdefinierte Code bei Auslösung mehrerer Tokenrückrufe nur einmal ausgeführt wird, überprüft die Beispielimplementierung Dateihashes. Das Beispiel verwendet SHA1-Dateihashing. Eine Wiederholung wird mit einem exponentiellen Backoff implementiert.

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];
}

Einfaches Starten von Änderungstoken

Registrieren Sie einen Action-Rückruf eines Tokenconsumers für Änderungsbenachrichtigungen an das Token zum Neuladen der Konfiguration.

In Startup.Configure:

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

config.GetReloadToken() stellt das Token bereit. Der Rückruf ist die InvokeChanged-Methode:

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

Der state des Rückrufs wird zum Übergeben von IHostingEnvironment verwendet. Dies ist hilfreich, um die richtige appsettings-Konfigurationsdatei anzugeben, die überwacht werden soll (in der Entwicklungsumgebung beispielsweise appsettings.Development.json). Dateihashes werden verwendet, um zu verhindern, dass die WriteConsole-Anweisung mehrere Male ausgeführt wird. Dies liegt an mehreren Tokenrückrufen, wenn die Konfigurationsdatei nur einmal geändert wurde.

Dieses System wird so lange ausgeführt, wie die Anwendung ausgeführt wird. Es kann nicht vom Benutzer deaktiviert werden.

Überwachen von Konfigurationsänderungen als Dienst

Das Beispiel implementiert:

  • Grundlegendes Überwachen des Starttokens.
  • Überwachung als Dienst.
  • Ein Mechanismus zum Aktivieren und Deaktivieren des Monitoring.

In diesem Beispiel wird eine IConfigurationMonitor-Schnittstelle eingerichtet.

Extensions/ConfigurationMonitor.cs:

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

Der Konstruktor der implementierten ConfigurationMonitor-Klasse registriert einen Rückruf für Änderungsbenachrichtigungen:

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() stellt das Token bereit. InvokeChanged ist die Rückrufmethode. In dieser Instanz stellt state einen Verweis auf die IConfigurationMonitor-Instanz dar, die zum Zugriff auf den Überwachungsstatus verwendet wird. Es werden zwei Eigenschaften verwendet:

  • MonitoringEnabled: Diese Eigenschaft gibt an, ob der Rückruf den zugehörigen benutzerdefinierten Code ausführen soll.
  • CurrentState: Diese Eigenschaft beschreibt den aktuellen Überwachungsstatus zur Verwendung in der Benutzeroberfläche.

Die InvokeChanged-Methode ist vergleichbar mit dem früheren Ansatz, außer dass sie:

  • Den Code nicht ausführt, es sei denn, MonitoringEnabled ist true.
  • Den aktuellen state in der zugehörigen WriteConsole-Ausgabe berücksichtigt.
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}");
        }
    }
}

Eine ConfigurationMonitor-Instanz wird in Startup.ConfigureServices als Dienst registriert:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

Die Indexseite bietet das Benutzer Kontrolle über die Konfigurationsüberwachung. Die IConfigurationMonitor-Instanz wird in das IndexModel eingefügt.

Pages/Index.cshtml.cs:

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

Der Konfigurationsmonitor (_monitor) wird verwendet, um die Überwachung zu aktivieren oder zu deaktivieren und den aktuellen Status für Benutzeroberflächenfeedback festzulegen:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Wenn OnPostStartMonitoring ausgelöst wird, wird die Überwachung aktiviert, und der aktuelle Status ist deaktiviert. Wenn OnPostStopMonitoring ausgelöst wird, wird Überwachung deaktiviert, und der Zustand zeigt, dass die Überwachung nicht ausgeführt wird.

Die Überwachung wird über Schaltflächen in der Benutzeroberfläche aktiviert und deaktiviert.

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>

Überwachen von zwischengespeicherten Dateiänderungen

Dateiinhalt kann mithilfe von IMemoryCache speicherintern zwischengespeichert werden. Zwischenspeicherung im Arbeitsspeicher wird im Thema Cache in-memory (Zwischenspeicherung im Arbeitsspeicher) beschrieben. Ohne zusätzliche Schritte, wie der unten beschriebenen Implementierung, werden veraltete Daten aus einem Zwischenspeicher zurückgegeben, wenn sich die Quelldaten ändern.

Wird beispielsweise der Status einer zwischengespeicherten Quelldatei beim Erneuern eines variablen Ablaufzeitraums nicht berücksichtigt, führt dies zu veralteten Dateidaten im Cache. Jede Datenanforderung erneuert den variablen Ablaufzeitraum, aber die Datei wird nie neu in den Zwischenspeicher geladen. Alle Anwendungsfeatures, die den zwischengespeicherten Inhalt der Datei verwenden, erhalten möglicherweise veraltete Inhalte.

Durch die Verwendung von Änderungstoken in einem Szenario mit Dateizwischenspeicherung wird verhindert, dass veraltete Dateiinhalte im Zwischenspeicher auftreten. Die Beispielanwendung zeigt eine Implementierung des Ansatzes.

Das Beispiel verwendet GetFileContent, um:

  • Dateiinhalt zurückzugeben.
  • Implementieren eines Wiederholungsalgorithmus mit exponentiellem Backoff, um Fälle abzudecken, in denen ein Dateizugriffsproblem das Lesen des Dateiinhalts vorübergehend verzögert.

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;
}

Ein FileService wird erstellt, um die zwischengespeicherten Dateisuchvorgänge zu behandeln. Der Aufruf der GetFileContent-Methode des Diensts versucht, den Dateiinhalt aus dem speicherinternen Cache abzurufen und ihn an den Aufrufer zurückzugeben (Services/FileService.cs).

Wenn mithilfe des Cacheschlüssels zwischengespeicherte Inhalte nicht gefunden werden, werden folgende Aktionen durchgeführt:

  1. Der Inhalt der Datei wird mit GetFileContent abgerufen.
  2. Ein Änderungstoken wird mithilfe von IFileProviders.Watch aus dem Dateianbieter abgerufen. Wenn die Datei geändert wird, wird ein Rückruf des Tokens ausgelöst.
  3. Der Inhalt der Datei wird mit einem variablen Ablaufzeitraum zwischengespeichert. Das Änderungstoken wird mit MemoryCacheEntryExtensions.AddExpirationToken angefügt, um den Cacheeintrag zu entfernen, wenn die sich Datei ändert, während sie zwischengespeichert wird.

Im folgenden Beispiel werden Dateien im Inhaltsstammverzeichnis der App gespeichert. IHostingEnvironment.ContentRootFileProvider wird verwendet, um einen IFileProvider abzurufen, der auf den ContentRootPath der App zeigt. Der filePath wird mit IFileInfo.PhysicalPath abgerufen.

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;
    }
}

Der FileService ist zusammen mit dem Speichercachedienst im Dienstcontainer registriert:

In Startup.ConfigureServices:

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

Das Seitenmodell lädt mithilfe des Diensts den Inhalt der Datei.

In der OnGet-Methode auf der Indexseite (Pages/Index.cshtml.cs):

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

CompositeChangeToken-Klasse

Verwenden Sie für die Darstellung von einer oder mehreren IChangeToken-Instanzen in einem einzelnen Objekt die Klasse 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 für die kombinierten Tokenberichte ist true, wenn ein dargestelltes HasChanged-Token true ist. ActiveChangeCallbacks für die kombinierten Tokenberichte ist true, wenn ein dargestelltes ActiveChangeCallbacks-Token true ist. Wenn mehrere Änderungsereignisse gleichzeitig auftreten, wird der Rückruf für die kombinierte Änderung einmal aufgerufen.

Zusätzliche Ressourcen