Rilevare le modifiche apportate con i token di modifica in ASP.NET Core

Un token di modifica è un blocco predefinito di uso generico e di basso livello che viene usato per il rilevamento delle modifiche di stato.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Interfaccia IChangeToken

IChangeToken propaga le notifiche relative a una modifica apportata. IChangeToken risiede nello spazio dei nomi Microsoft.Extensions.Primitives. Il pacchetto NuGet Microsoft.Extensions.Primitives viene fornito in modo implicito alle app ASP.NET Core.

IChangeToken ha due proprietà:

  • ActiveChangeCallbacks indica se il token genera callback in modo proattivo. Se ActiveChangedCallbacks è impostato su false, non viene mai chiamato alcun callback e l'app deve eseguire il polling di HasChanged per ottenere le modifiche. È anche possibile che un token non venga mai annullato se non vengono apportate modifiche o se il listener di modifica sottostante viene eliminato o disabilitato.
  • HasChanged riceve un valore che indica se è stata apportata una modifica.

L'interfaccia IChangeToken include il metodo RegisterChangeCallback(Action<Object>, Object), che registra un callback richiamato quando il token è stato modificato. Prima che il callback venga richiamato è necessario impostare HasChanged.

Classe ChangeToken

ChangeToken è una classe statica usata per propagare le notifiche relative a una modifica che è stata apportata. ChangeToken risiede nello spazio dei nomi Microsoft.Extensions.Primitives. Il pacchetto NuGet Microsoft.Extensions.Primitives viene fornito in modo implicito alle app ASP.NET Core.

Il metodo ChangeToken.OnChange(Func<IChangeToken>, Action) registra un oggetto Action da chiamare ogni volta che il token cambia:

  • Func<IChangeToken> produce il token.
  • Action viene chiamato alla modifica del token.

L'overload ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) accetta un parametro aggiuntivo TState passato al consumer Actiondel token .

OnChange restituisce un oggetto IDisposable. La chiamata a Dispose interrompe l'ascolto di altre modifiche da parte del token e rilascia le risorse del token.

Esempi di uso di token di modifica in ASP.NET Core

I token di modifica vengono usati nelle aree principali di ASP.NET Core per monitorare le modifiche apportate agli oggetti:

  • Per monitorare le modifiche ai file, il metodo Watch di IFileProvider crea un elemento IChangeToken per le cartelle o i file specificati da controllare.
  • È possibile aggiungere token IChangeToken alle voci della cache per attivare le eliminazioni dalla cache alla modifica.
  • Per le modifiche di TOptions, l'implementazione OptionsMonitor<TOptions> predefinita di IOptionsMonitor<TOptions> ha un overload che accetta una o più istanze di IOptionsChangeTokenSource<TOptions>. Ogni istanza restituisce un elemento IChangeToken per registrare un callback di notifica delle modifiche per il rilevamento delle modifiche apportate alle opzioni.

Monitorare le modifiche di configurazione

Per impostazione predefinita, i modelli di base ASP.NET usano JSi file di configurazione ON (appsettings.json, appsettings.Development.jsone appsettings.Production.json) per caricare le impostazioni di configurazione dell'app.

Questi file vengono configurati usando il metodo di estensione AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) per ConfigurationBuilder che accetta un parametro reloadOnChange. reloadOnChange indica se la configurazione deve essere ricaricata alla modifica del file. Questa impostazione viene visualizzata nel metodo pratico HostCreateDefaultBuilder:

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

La configurazione basata su file è rappresentata da FileConfigurationSource. FileConfigurationSource usa IFileProvider per monitorare i file.

Per impostazione predefinita, IFileMonitor viene offerto da una classe PhysicalFileProvider che usa FileSystemWatcher per monitorare le modifiche apportate al file di configurazione.

L'app di esempio illustra due implementazioni per il monitoraggio delle modifiche alla configurazione. Se uno dei appsettings file cambia, entrambe le implementazioni di monitoraggio dei file eseguono codice personalizzato, l'app di esempio scrive un messaggio nella console.

L'elemento FileSystemWatcher di un file di configurazione può attivare più callback del token per una singola modifica del file di configurazione. Per assicurarsi che il codice personalizzato venga eseguito solo una volta quando vengono attivati più callback del token, l'implementazione dell'esempio controlla gli hash dei file. L'esempio usa l'hash di file SHA1. Viene implementato un nuovo tentativo con un'interruzione temporanea esponenziale.

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

Semplice token di modifica dell'avvio

Registrare un callback dell'elemento Action consumer del token per le notifiche di modifica al token di ricaricamento della configurazione.

In Startup.Configure:

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

config.GetReloadToken() fornisce il token. Il callback è il metodo 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)");
    }
}

L'oggetto state del callback viene usato per passare l'oggetto IWebHostEnvironment, utile per specificare il file di configurazione corretto appsettings da monitorare, appsettings.Development.json ad esempio quando si trova nell'ambiente di sviluppo. Gli hash dei file vengono usati per impedire che l'istruzione WriteConsole venga eseguita più volte a causa di più callback del token quando il file di configurazione è stato modificato una sola volta.

Questo sistema rimane in esecuzione finché l'app è in esecuzione e non può essere disabilitato dall'utente.

Monitorare le modifiche di configurazione come servizio

L'esempio implementa:

  • Monitoraggio di base del token di avvio.
  • Monitoraggio come servizio.
  • Un meccanismo per abilitare e disabilitare il monitoraggio.

L'esempio stabilisce un'interfaccia IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

Il costruttore della classe implementata, ConfigurationMonitor, registra un callback per le notifiche di modifica:

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() fornisce il token. InvokeChanged è il metodo di callback. Il valore state in questa istanza è un riferimento all'istanza IConfigurationMonitor usata per accedere allo stato di monitoraggio. Vengono usate due proprietà:

  • MonitoringEnabled: indica se il callback deve eseguire il codice personalizzato.
  • CurrentState: descrive lo stato di monitoraggio corrente da usare nell'interfaccia utente.

Il metodo InvokeChanged è simile all'approccio precedente, ad eccezione del fatto che:

  • Non esegue il proprio codice a meno che MonitoringEnabled non sia impostato su true.
  • Restituisce l'elemento state corrente nell'output 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}");
        }
    }
}

Un'istanza di ConfigurationMonitor viene registrata come servizio in Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

La pagina di indice offre all'utente il controllo sul monitoraggio della configurazione. L'istanza di IConfigurationMonitor viene inserita in IndexModel.

Pages/Index.cshtml.cs:

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

Il monitoraggio di configurazione (_monitor) viene usato per abilitare o disabilitare il monitoraggio e impostare lo stato corrente per commenti e suggerimenti dell'interfaccia utente:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quando viene attivato OnPostStartMonitoring, il monitoraggio è abilitato e lo stato corrente viene cancellato. Quando viene attivato OnPostStopMonitoring, il monitoraggio è disabilitato e lo stato viene impostato in modo da indicare che il monitoraggio non viene eseguito.

I pulsanti nell'interfaccia utente abilitano e disabilitano il monitoraggio.

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>

Monitorare le modifiche al file nella cache

È possibile memorizzare il contenuto del file nella cache in memoria usando IMemoryCache. La memorizzazione nella cache in memoria è descritta nell'argomento Cache in memoria. Senza eseguire passaggi aggiuntivi, ad esempio l'implementazione descritta di seguito, la cache restituirà dati non aggiornati (obsoleti) se i dati di origine vengono modificati.

Ad esempio, se durante il rinnovo di un periodo di scadenza variabile non si tiene conto dello stato di un file di origine memorizzato nella cache, i dati relativi ai file nella cache risulteranno non aggiornati. Ogni richiesta di dati rinnoverà il periodo di scadenza variabile, ma il file non verrà mai ricaricato nella cache. Le funzionalità dell'app che usano il contenuto del file memorizzato nella cache sono soggette alla possibile ricezione di contenuto non aggiornato.

L'uso dei token di modifica in uno scenario di memorizzazione di file nella cache consente di evitare la presenza di contenuto dei file non aggiornato nella cache. L'app di esempio illustra un'implementazione dell'approccio.

Nell'esempio viene usato GetFileContent per:

  • Restituire il contenuto del file.
  • Implementare un algoritmo di ripetizione dei tentativi con back-off esponenziale per coprire i casi in cui un problema di accesso ai file ritarda temporaneamente la lettura del contenuto del file.

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

Viene creato un elemento FileService per gestire le ricerche nel file memorizzato nella cache. La GetFileContent chiamata al metodo del servizio tenta di ottenere il contenuto del file dalla cache in memoria e di restituirlo al chiamante (Services/FileService.cs).

Se non è possibile reperire il contenuto memorizzato nella cache usando la chiave della cache, verranno intraprese le azioni seguenti:

  1. Il contenuto del file viene ottenuto usando GetFileContent.
  2. Un token di modifica viene ottenuto dal provider di file con IFileProviders.Watch. Il callback del token viene attivato alla modifica del file.
  3. Il contenuto del file viene memorizzato nella cache con un periodo di scadenza variabile. Al token di modifica viene associato MemoryCacheEntryExtensions.AddExpirationToken per eliminare la voce della cache se il file viene modificato mentre è memorizzato nella cache.

Nell'esempio seguente i file vengono archiviati nella radice del contenuto dell'app. IWebHostEnvironment.ContentRootFileProvider viene usato per ottenere un IFileProvider puntatore all'oggetto dell'app IWebHostEnvironment.ContentRootPath. filePath viene ottenuto con IFileInfo.PhysicalPath.

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 è registrato nel contenitore di servizi insieme al servizio di memorizzazione nella cache.

In Startup.ConfigureServices:

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

Il modello di pagina carica il contenuto del file usando il servizio.

Nel metodo della OnGet pagina Index (Pages/Index.cshtml.cs):

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

Classe CompositeChangeToken

Per rappresentare una o più istanze di IChangeToken in un singolo oggetto, usare la classe 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 nel token composito indica true se l'elemento HasChanged di qualsiasi token rappresentato è true. ActiveChangeCallbacks nel token composito indica true se l'elemento ActiveChangeCallbacks di qualsiasi token rappresentato è true. Se si verificano più eventi di modifica simultanei, il callback della modifica composita viene richiamato una volta.

Un token di modifica è un blocco predefinito di uso generico e di basso livello che viene usato per il rilevamento delle modifiche di stato.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Interfaccia IChangeToken

IChangeToken propaga le notifiche relative a una modifica apportata. IChangeToken risiede nello spazio dei nomi Microsoft.Extensions.Primitives. Per le app che non usano il metapacchetto Microsoft.AspNetCore.App, creare un riferimento al pacchetto per il pacchetto NuGet Microsoft.Extensions.Primitives.

IChangeToken ha due proprietà:

  • ActiveChangeCallbacks indica se il token genera callback in modo proattivo. Se ActiveChangedCallbacks è impostato su false, non viene mai chiamato alcun callback e l'app deve eseguire il polling di HasChanged per ottenere le modifiche. È anche possibile che un token non venga mai annullato se non vengono apportate modifiche o se il listener di modifica sottostante viene eliminato o disabilitato.
  • HasChanged riceve un valore che indica se è stata apportata una modifica.

L'interfaccia IChangeToken include il metodo RegisterChangeCallback(Action<Object>, Object), che registra un callback richiamato quando il token è stato modificato. Prima che il callback venga richiamato è necessario impostare HasChanged.

Classe ChangeToken

ChangeToken è una classe statica usata per propagare le notifiche relative a una modifica che è stata apportata. ChangeToken risiede nello spazio dei nomi Microsoft.Extensions.Primitives. Per le app che non usano il metapacchetto Microsoft.AspNetCore.App, creare un riferimento al pacchetto per il pacchetto NuGet Microsoft.Extensions.Primitives.

Il metodo ChangeToken.OnChange(Func<IChangeToken>, Action) registra un oggetto Action da chiamare ogni volta che il token cambia:

  • Func<IChangeToken> produce il token.
  • Action viene chiamato alla modifica del token.

L'overload ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) accetta un parametro aggiuntivo TState passato al consumer Actiondel token .

OnChange restituisce un oggetto IDisposable. La chiamata a Dispose interrompe l'ascolto di altre modifiche da parte del token e rilascia le risorse del token.

Esempi di uso di token di modifica in ASP.NET Core

I token di modifica vengono usati nelle aree principali di ASP.NET Core per monitorare le modifiche apportate agli oggetti:

  • Per monitorare le modifiche ai file, il metodo Watch di IFileProvider crea un elemento IChangeToken per le cartelle o i file specificati da controllare.
  • È possibile aggiungere token IChangeToken alle voci della cache per attivare le eliminazioni dalla cache alla modifica.
  • Per le modifiche di TOptions, l'implementazione OptionsMonitor<TOptions> predefinita di IOptionsMonitor<TOptions> ha un overload che accetta una o più istanze di IOptionsChangeTokenSource<TOptions>. Ogni istanza restituisce un elemento IChangeToken per registrare un callback di notifica delle modifiche per il rilevamento delle modifiche apportate alle opzioni.

Monitorare le modifiche di configurazione

Per impostazione predefinita, i modelli di base ASP.NET usano JSi file di configurazione ON (appsettings.json, appsettings.Development.jsone appsettings.Production.json) per caricare le impostazioni di configurazione dell'app.

Questi file vengono configurati usando il metodo di estensione AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) per ConfigurationBuilder che accetta un parametro reloadOnChange. reloadOnChange indica se la configurazione deve essere ricaricata alla modifica del file. Questa impostazione viene visualizzata nel metodo pratico WebHostCreateDefaultBuilder:

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

La configurazione basata su file è rappresentata da FileConfigurationSource. FileConfigurationSource usa IFileProvider per monitorare i file.

Per impostazione predefinita, IFileMonitor viene offerto da una classe PhysicalFileProvider che usa FileSystemWatcher per monitorare le modifiche apportate al file di configurazione.

L'app di esempio illustra due implementazioni per il monitoraggio delle modifiche alla configurazione. Se uno dei appsettings file cambia, entrambe le implementazioni di monitoraggio dei file eseguono codice personalizzato, l'app di esempio scrive un messaggio nella console.

L'elemento FileSystemWatcher di un file di configurazione può attivare più callback del token per una singola modifica del file di configurazione. Per assicurarsi che il codice personalizzato venga eseguito solo una volta quando vengono attivati più callback del token, l'implementazione dell'esempio controlla gli hash dei file. L'esempio usa l'hash di file SHA1. Viene implementato un nuovo tentativo con un'interruzione temporanea esponenziale.

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

Semplice token di modifica dell'avvio

Registrare un callback dell'elemento Action consumer del token per le notifiche di modifica al token di ricaricamento della configurazione.

In Startup.Configure:

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

config.GetReloadToken() fornisce il token. Il callback è il metodo 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)");
    }
}

L'oggetto state del callback viene usato per passare l'oggetto IHostingEnvironment, utile per specificare il file di configurazione corretto appsettings da monitorare, appsettings.Development.json ad esempio quando si trova nell'ambiente di sviluppo. Gli hash dei file vengono usati per impedire che l'istruzione WriteConsole venga eseguita più volte a causa di più callback del token quando il file di configurazione è stato modificato una sola volta.

Questo sistema rimane in esecuzione finché l'app è in esecuzione e non può essere disabilitato dall'utente.

Monitorare le modifiche di configurazione come servizio

L'esempio implementa:

  • Monitoraggio di base del token di avvio.
  • Monitoraggio come servizio.
  • Un meccanismo per abilitare e disabilitare il monitoraggio.

L'esempio stabilisce un'interfaccia IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

Il costruttore della classe implementata, ConfigurationMonitor, registra un callback per le notifiche di modifica:

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() fornisce il token. InvokeChanged è il metodo di callback. Il valore state in questa istanza è un riferimento all'istanza IConfigurationMonitor usata per accedere allo stato di monitoraggio. Vengono usate due proprietà:

  • MonitoringEnabled: indica se il callback deve eseguire il codice personalizzato.
  • CurrentState: descrive lo stato di monitoraggio corrente da usare nell'interfaccia utente.

Il metodo InvokeChanged è simile all'approccio precedente, ad eccezione del fatto che:

  • Non esegue il proprio codice a meno che MonitoringEnabled non sia impostato su true.
  • Restituisce l'elemento state corrente nell'output 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}");
        }
    }
}

Un'istanza di ConfigurationMonitor viene registrata come servizio in Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

La pagina di indice offre all'utente il controllo sul monitoraggio della configurazione. L'istanza di IConfigurationMonitor viene inserita in IndexModel.

Pages/Index.cshtml.cs:

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

Il monitoraggio di configurazione (_monitor) viene usato per abilitare o disabilitare il monitoraggio e impostare lo stato corrente per commenti e suggerimenti dell'interfaccia utente:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quando viene attivato OnPostStartMonitoring, il monitoraggio è abilitato e lo stato corrente viene cancellato. Quando viene attivato OnPostStopMonitoring, il monitoraggio è disabilitato e lo stato viene impostato in modo da indicare che il monitoraggio non viene eseguito.

I pulsanti nell'interfaccia utente abilitano e disabilitano il monitoraggio.

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>

Monitorare le modifiche al file nella cache

È possibile memorizzare il contenuto del file nella cache in memoria usando IMemoryCache. La memorizzazione nella cache in memoria è descritta nell'argomento Cache in memoria. Senza eseguire passaggi aggiuntivi, ad esempio l'implementazione descritta di seguito, la cache restituirà dati non aggiornati (obsoleti) se i dati di origine vengono modificati.

Ad esempio, se durante il rinnovo di un periodo di scadenza variabile non si tiene conto dello stato di un file di origine memorizzato nella cache, i dati relativi ai file nella cache risulteranno non aggiornati. Ogni richiesta di dati rinnoverà il periodo di scadenza variabile, ma il file non verrà mai ricaricato nella cache. Le funzionalità dell'app che usano il contenuto del file memorizzato nella cache sono soggette alla possibile ricezione di contenuto non aggiornato.

L'uso dei token di modifica in uno scenario di memorizzazione di file nella cache consente di evitare la presenza di contenuto dei file non aggiornato nella cache. L'app di esempio illustra un'implementazione dell'approccio.

Nell'esempio viene usato GetFileContent per:

  • Restituire il contenuto del file.
  • Implementare un algoritmo di ripetizione dei tentativi con back-off esponenziale per coprire i casi in cui un problema di accesso ai file ritarda temporaneamente la lettura del contenuto del file.

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

Viene creato un elemento FileService per gestire le ricerche nel file memorizzato nella cache. La GetFileContent chiamata al metodo del servizio tenta di ottenere il contenuto del file dalla cache in memoria e di restituirlo al chiamante (Services/FileService.cs).

Se non è possibile reperire il contenuto memorizzato nella cache usando la chiave della cache, verranno intraprese le azioni seguenti:

  1. Il contenuto del file viene ottenuto usando GetFileContent.
  2. Un token di modifica viene ottenuto dal provider di file con IFileProviders.Watch. Il callback del token viene attivato alla modifica del file.
  3. Il contenuto del file viene memorizzato nella cache con un periodo di scadenza variabile. Al token di modifica viene associato MemoryCacheEntryExtensions.AddExpirationToken per eliminare la voce della cache se il file viene modificato mentre è memorizzato nella cache.

Nell'esempio seguente i file vengono archiviati nella radice del contenuto dell'app. IHostingEnvironment.ContentRootFileProvider viene usato per ottenere un IFileProvider che punta al ContentRootPath dell'app. filePath viene ottenuto con IFileInfo.PhysicalPath.

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 è registrato nel contenitore di servizi insieme al servizio di memorizzazione nella cache.

In Startup.ConfigureServices:

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

Il modello di pagina carica il contenuto del file usando il servizio.

Nel metodo della OnGet pagina Index (Pages/Index.cshtml.cs):

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

Classe CompositeChangeToken

Per rappresentare una o più istanze di IChangeToken in un singolo oggetto, usare la classe 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 nel token composito indica true se l'elemento HasChanged di qualsiasi token rappresentato è true. ActiveChangeCallbacks nel token composito indica true se l'elemento ActiveChangeCallbacks di qualsiasi token rappresentato è true. Se si verificano più eventi di modifica simultanei, il callback della modifica composita viene richiamato una volta.

Risorse aggiuntive