Détecter les modifications avec des jetons de modification dans ASP.NET Core

Un jeton de modification est un module à usage général de bas niveau, utilisé pour effectuer le suivi des modifications de l’état.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Interface d’IChangeToken

IChangeToken propage des notifications indiquant qu’une modification s’est produite. IChangeToken réside dans l’espace de noms Microsoft.Extensions.Primitives. Le package NuGet Microsoft.Extensions.Primitives est implicitement fourni aux applications ASP.NET Core.

IChangeToken a deux propriétés :

  • ActiveChangeCallbacks indique si le jeton déclenche des rappels de façon proactive. Si ActiveChangedCallbacks est défini sur false, un rappel n’est jamais appelé, et l’application doit interroger HasChanged pour connaître les modifications. Il est également possible qu’un jeton ne soit jamais annulé si aucune modification ne se produit, ou si l’écouteur de modifications sous-jacent est supprimé ou désactivé.
  • HasChanged obtient une valeur qui indique si une modification a eu lieu.

L’interface IChangeToken inclut la méthode RegisterChangeCallback(Action<Objet>, Objet)>, qui inscrit un rappel qui est appelé quand le jeton a été modifié. HasChanged doit être défini avant que le rappel soit appelé.

Classe ChangeToken

ChangeToken est une classe statique utilisée pour propager des notifications indiquant qu’une modification s’est produite. ChangeToken réside dans l’espace de noms Microsoft.Extensions.Primitives. Le package NuGet Microsoft.Extensions.Primitives est implicitement fourni aux applications ASP.NET Core.

La méthode ChangeToken.OnChange(Func<IChangeToken>, Action)> inscrit une à appelerAction chaque fois que le jeton change :

  • Func<IChangeToken> produit le jeton.
  • Action est appelée quand le jeton change.

La surcharge ChangeToken.OnChange<TState>>(Func<IChangeToken>>, Action<TState>, TState) prend un paramètre TState supplémentaire qui est passé dans le contrôle serveur consommateur du jetonAction.

OnChangeRetourne un IDisposable. L’appel de Dispose fait que le jeton cesse d’être à l’écoute des modifications suivantes et libère les ressources du jeton.

Exemples d’utilisation de jetons de modification dans ASP.NET Core

Les jetons de modification sont utilisés dans des zones importantes d’ASP.NET Core pour surveiller les modifications apportées aux objets :

  • Pour surveiller les modifications apportées aux fichiers, la méthode Watch de IFileProvider crée un IChangeToken pour les fichiers ou le dossier à surveiller.
  • Les jetons IChangeToken peuvent être ajoutés aux entrées du cache pour déclencher des suppressions dans le cache en cas de modification.
  • Pour les modifications TOptions, l’implémentation OptionsMonitor<TOptions> par défaut de IOptionsMonitor<TOptions> a une surcharge qui accepte une ou plusieurs instances IOptionsChangeTokenSource<TOptions>. Chaque instance retourne un IChangeToken pour inscrire un rappel de notification de modification pour les modifications des options de suivi.

Surveiller les modifications de configuration

Par défaut, ASP.NET modèles Principaux utilisent JSdes fichiers de configuration ON (appsettings.json, appsettings.Development.jsonet appsettings.Production.json) pour charger les paramètres de configuration de l’application.

Ces fichiers sont configurés avec la méthode d’extension AddJsonFile(IConfigurationBuilder, chaîne, booléen, booléen) sur ConfigurationBuilder qui accepte un paramètre reloadOnChange. reloadOnChange indique si la configuration doit être rechargée en cas de modification d’un fichier. Ce paramètre s’affiche dans la méthode pratique HostCreateDefaultBuilder :

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

La configuration basée sur les fichiers est représentée par FileConfigurationSource. FileConfigurationSource utilise IFileProvider pour surveiller les fichiers.

Par défaut, IFileMonitor est fourni par un PhysicalFileProvider, qui utilise FileSystemWatcher pour surveiller les modifications des fichiers de configuration.

L’exemple d’application montre les deux implémentations de la surveillance des modifications de configuration. Si l’un des fichiers appsettings change, les deux implémentations d’analyse du fichier exécutent du code personnalisé—l’exemple d’application écrit un message dans la console.

Le FileSystemWatcher d’un fichier de configuration peut déclencher plusieurs rappels de jeton pour une même modification du fichier de configuration. Pour vous assurer que le code personnalisé n’est exécuté qu’une seule fois lors du déclenchement de plusieurs rappels de jeton, l’implémentation de l’exemple vérifie les hachages des fichiers. L’exemple utilise le hachage de fichier SHA1. Une nouvelle tentative est implémentée avec une interruption d’une durée exponentielle.

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

Jeton de modification de démarrage simple

Inscrivez un rappel Action de consommateur de jeton pour les notifications de modification au jeton de rechargement de configuration.

Dans Startup.Configure :

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

config.GetReloadToken() fournit le jeton. Le rappel est la méthode 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)");
    }
}

Le state du rappel est utilisé pour passer le IWebHostEnvironment, ce qui est utile pour spécifier le fichier de configuration de appsettings approprié à surveiller (par exemple, appsettings.Development.json quand dans l’environnement de développement). Les hachages de fichier sont utilisés pour empêcher l’instruction WriteConsole d’être exécutée plusieurs fois en raison de plusieurs rappels de jeton alors que le fichier de configuration n’a été modifié qu’une seule fois.

Ce système s’exécute tant que l’application est en cours d’exécution et ne peut pas être désactivé par l’utilisateur.

Surveiller les modifications de configuration en tant que service

L’exemple implémente :

  • Surveillance de jeton du démarrage de base.
  • Surveillance en tant que service.
  • Un mécanisme pour activer et désactiver la surveillance.

L’exemple établit une interface IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

Le constructeur de la classe implémentée, ConfigurationMonitor, inscrit un rappel pour les notifications de modification :

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() fournit le jeton. InvokeChanged est la méthode de rappel. Le state dans cette instance est une référence à l’instance IConfigurationMonitor qui est utilisée pour accéder à l’état du monitoring. Deux propriétés sont utilisées :

  • MonitoringEnabled: Indique si le rappel doit exécuter son code personnalisé.
  • CurrentStateDécrit l’état actuel de la surveillance pour une utilisation dans l’interface utilisateur.

La méthode InvokeChanged est similaire à l’approche précédente, excepté que :

  • Elle n’exécute pas son code, sauf si MonitoringEnabled est true.
  • Elle indique le state actuel dans sa sortie 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}");
        }
    }
}

Une instance ConfigurationMonitor est inscrite en tant que service dans Startup.ConfigureServices :

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

La page Index permet à l’utilisateur de contrôler la surveillance de la configuration. L’instance de IConfigurationMonitor est injectée dans le IndexModel.

Pages/Index.cshtml.cs:

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

Le moniteur de configuration (_monitor) est utilisé pour activer ou désactiver l’analyse et définir l’état actuel pour les commentaires sur l’interface utilisateur :

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quand OnPostStartMonitoring est déclenché, la surveillance est activée et l’état actuel est effacé. Quand OnPostStopMonitoring est déclenché, la surveillance est désactivée et l’état est défini de façon à indiquer que la surveillance n’est pas effectuée.

Des boutons dans l’IU activent et désactivent la surveillance.

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>

Surveiller les modifications de fichiers mis en cache

Le contenu des fichiers peut être mis en cache en mémoire avec IMemoryCache. La mise en cache en mémoire est décrite dans la rubrique Mise en cache en mémoire. Sans actions supplémentaires, comme l’implémentation décrite ci-dessous, les données périmées (obsolètes) sont retournées depuis un cache si la source de données change.

Par exemple, le fait de ne pas prendre en compte l’état d’un fichier source en cache lors du renouvellement d’une période d’expiration décalée rend obsolètes les données du fichier mis cache. Chaque demande de données renouvelle la période d’expiration décalée, mais le fichier n’est jamais rechargé dans le cache. Toutes les fonctionnalités d’une application qui utilisent le contenu mis en cache d’un fichier sont exposées au risque de recevoir du contenu obsolète.

L’utilisation de jetons de modification dans un scénario de mise en cache de fichier empêche la présence de contenu obsolète dans le cache. L’exemple d’application montre une implémentation de l’approche.

L’exemple utilise GetFileContent pour :

  • Retourner le contenu du fichier.
  • Implémentez un algorithme de nouvelle tentative avec interruption exponentielle pour couvrir les cas où un problème d’accès au fichier retarde temporairement la lecture du contenu du fichier.

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

Un FileService est créé pour gérer les recherches des fichiers mis en cache. L’appel de méthode GetFileContent du service tente d’obtenir le contenu du fichier à partir du cache en mémoire et de le renvoyer à l’appelant (Services/FileService.cs).

Si le contenu en cache n’est pas trouvé avec la clé du cache, les actions suivantes sont effectuées :

  1. Le contenu du fichier est obtenu à l’aide de GetFileContent.
  2. Un jeton de modification est obtenu auprès du fournisseur du fichier avec IFileProviders.Watch. Le rappel du jeton est déclenché quand le fichier est modifié.
  3. Le contenu du fichier est mis en cache avec une période d’expiration décalée. Le jeton de modification est attaché avec MemoryCacheEntryExtensions.AddExpirationToken pour supprimer l’entrée du cache si le fichier change alors qu’il est mis en cache.

Dans l’exemple suivant, des fichiers sont stockés à la racine du contenu de l’application. IWebHostEnvironment.ContentRootFileProvider est utilisé pour obtenir un IFileProvider pointage au niveau de IWebHostEnvironment.ContentRootPathl’application. filePath est obtenue avec 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 est inscrit dans le conteneur de service, ainsi que le service de mise en cache en mémoire.

Dans Startup.ConfigureServices :

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

Le modèle de page charge le contenu du fichier en utilisant le service.

Dans la méthode de OnGet la page Index (Pages/Index.cshtml.cs) :

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

Classe CompositeChangeToken

Pour représenter une ou plusieurs instances de IChangeToken dans un même objet, utilisez 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 sur le jeton composite indique true si l’élément HasChanged d’un jeton représenté est true. ActiveChangeCallbacks sur le jeton composite indique true si l’élément ActiveChangeCallbacks d’un jeton représenté est true. Si plusieurs modifications simultanées se produisent, le rappel de modification composite n’est appelé qu’une fois.

Un jeton de modification est un module à usage général de bas niveau, utilisé pour effectuer le suivi des modifications de l’état.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Interface d’IChangeToken

IChangeToken propage des notifications indiquant qu’une modification s’est produite. IChangeToken réside dans l’espace de noms Microsoft.Extensions.Primitives. Pour les applications qui n’utilisent pas le métapaquet Microsoft.AspNetCore.App, créez une référence au package NuGet Microsoft.Extensions.Primitives.

IChangeToken a deux propriétés :

  • ActiveChangeCallbacks indique si le jeton déclenche des rappels de façon proactive. Si ActiveChangedCallbacks est défini sur false, un rappel n’est jamais appelé, et l’application doit interroger HasChanged pour connaître les modifications. Il est également possible qu’un jeton ne soit jamais annulé si aucune modification ne se produit, ou si l’écouteur de modifications sous-jacent est supprimé ou désactivé.
  • HasChanged obtient une valeur qui indique si une modification a eu lieu.

L’interface IChangeToken inclut la méthode RegisterChangeCallback(Action<Objet>, Objet)>, qui inscrit un rappel qui est appelé quand le jeton a été modifié. HasChanged doit être défini avant que le rappel soit appelé.

Classe ChangeToken

ChangeToken est une classe statique utilisée pour propager des notifications indiquant qu’une modification s’est produite. ChangeToken réside dans l’espace de noms Microsoft.Extensions.Primitives. Pour les applications qui n’utilisent pas le métapaquet Microsoft.AspNetCore.App, créez une référence au package NuGet Microsoft.Extensions.Primitives.

La méthode ChangeToken.OnChange(Func<IChangeToken>, Action)> inscrit une à appelerAction chaque fois que le jeton change :

  • Func<IChangeToken> produit le jeton.
  • Action est appelée quand le jeton change.

La surcharge ChangeToken.OnChange<TState>>(Func<IChangeToken>>, Action<TState>, TState) prend un paramètre TState supplémentaire qui est passé dans le contrôle serveur consommateur du jetonAction.

OnChangeRetourne un IDisposable. L’appel de Dispose fait que le jeton cesse d’être à l’écoute des modifications suivantes et libère les ressources du jeton.

Exemples d’utilisation de jetons de modification dans ASP.NET Core

Les jetons de modification sont utilisés dans des zones importantes d’ASP.NET Core pour surveiller les modifications apportées aux objets :

  • Pour surveiller les modifications apportées aux fichiers, la méthode Watch de IFileProvider crée un IChangeToken pour les fichiers ou le dossier à surveiller.
  • Les jetons IChangeToken peuvent être ajoutés aux entrées du cache pour déclencher des suppressions dans le cache en cas de modification.
  • Pour les modifications TOptions, l’implémentation OptionsMonitor<TOptions> par défaut de IOptionsMonitor<TOptions> a une surcharge qui accepte une ou plusieurs instances IOptionsChangeTokenSource<TOptions>. Chaque instance retourne un IChangeToken pour inscrire un rappel de notification de modification pour les modifications des options de suivi.

Surveiller les modifications de configuration

Par défaut, ASP.NET modèles Principaux utilisent JSdes fichiers de configuration ON (appsettings.json, appsettings.Development.jsonet appsettings.Production.json) pour charger les paramètres de configuration de l’application.

Ces fichiers sont configurés avec la méthode d’extension AddJsonFile(IConfigurationBuilder, chaîne, booléen, booléen) sur ConfigurationBuilder qui accepte un paramètre reloadOnChange. reloadOnChange indique si la configuration doit être rechargée en cas de modification d’un fichier. Ce paramètre s’affiche dans la méthode pratique WebHostCreateDefaultBuilder :

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

La configuration basée sur les fichiers est représentée par FileConfigurationSource. FileConfigurationSource utilise IFileProvider pour surveiller les fichiers.

Par défaut, IFileMonitor est fourni par un PhysicalFileProvider, qui utilise FileSystemWatcher pour surveiller les modifications des fichiers de configuration.

L’exemple d’application montre les deux implémentations de la surveillance des modifications de configuration. Si l’un des fichiers appsettings change, les deux implémentations d’analyse du fichier exécutent du code personnalisé—l’exemple d’application écrit un message dans la console.

Le FileSystemWatcher d’un fichier de configuration peut déclencher plusieurs rappels de jeton pour une même modification du fichier de configuration. Pour vous assurer que le code personnalisé n’est exécuté qu’une seule fois lors du déclenchement de plusieurs rappels de jeton, l’implémentation de l’exemple vérifie les hachages des fichiers. L’exemple utilise le hachage de fichier SHA1. Une nouvelle tentative est implémentée avec une interruption d’une durée exponentielle.

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

Jeton de modification de démarrage simple

Inscrivez un rappel Action de consommateur de jeton pour les notifications de modification au jeton de rechargement de configuration.

Dans Startup.Configure :

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

config.GetReloadToken() fournit le jeton. Le rappel est la méthode 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)");
    }
}

Le state du rappel est utilisé pour passer le IHostingEnvironment, ce qui est utile pour spécifier le fichier de configuration de appsettings approprié à surveiller (par exemple, appsettings.Development.json quand dans l’environnement de développement). Les hachages de fichier sont utilisés pour empêcher l’instruction WriteConsole d’être exécutée plusieurs fois en raison de plusieurs rappels de jeton alors que le fichier de configuration n’a été modifié qu’une seule fois.

Ce système s’exécute tant que l’application est en cours d’exécution et ne peut pas être désactivé par l’utilisateur.

Surveiller les modifications de configuration en tant que service

L’exemple implémente :

  • Surveillance de jeton du démarrage de base.
  • Surveillance en tant que service.
  • Un mécanisme pour activer et désactiver la surveillance.

L’exemple établit une interface IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

Le constructeur de la classe implémentée, ConfigurationMonitor, inscrit un rappel pour les notifications de modification :

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() fournit le jeton. InvokeChanged est la méthode de rappel. Le state dans cette instance est une référence à l’instance IConfigurationMonitor qui est utilisée pour accéder à l’état du monitoring. Deux propriétés sont utilisées :

  • MonitoringEnabled: Indique si le rappel doit exécuter son code personnalisé.
  • CurrentStateDécrit l’état actuel de la surveillance pour une utilisation dans l’interface utilisateur.

La méthode InvokeChanged est similaire à l’approche précédente, excepté que :

  • Elle n’exécute pas son code, sauf si MonitoringEnabled est true.
  • Elle indique le state actuel dans sa sortie 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}");
        }
    }
}

Une instance ConfigurationMonitor est inscrite en tant que service dans Startup.ConfigureServices :

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

La page Index permet à l’utilisateur de contrôler la surveillance de la configuration. L’instance de IConfigurationMonitor est injectée dans le IndexModel.

Pages/Index.cshtml.cs:

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

Le moniteur de configuration (_monitor) est utilisé pour activer ou désactiver l’analyse et définir l’état actuel pour les commentaires sur l’interface utilisateur :

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quand OnPostStartMonitoring est déclenché, la surveillance est activée et l’état actuel est effacé. Quand OnPostStopMonitoring est déclenché, la surveillance est désactivée et l’état est défini de façon à indiquer que la surveillance n’est pas effectuée.

Des boutons dans l’IU activent et désactivent la surveillance.

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>

Surveiller les modifications de fichiers mis en cache

Le contenu des fichiers peut être mis en cache en mémoire avec IMemoryCache. La mise en cache en mémoire est décrite dans la rubrique Mise en cache en mémoire. Sans actions supplémentaires, comme l’implémentation décrite ci-dessous, les données périmées (obsolètes) sont retournées depuis un cache si la source de données change.

Par exemple, le fait de ne pas prendre en compte l’état d’un fichier source en cache lors du renouvellement d’une période d’expiration décalée rend obsolètes les données du fichier mis cache. Chaque demande de données renouvelle la période d’expiration décalée, mais le fichier n’est jamais rechargé dans le cache. Toutes les fonctionnalités d’une application qui utilisent le contenu mis en cache d’un fichier sont exposées au risque de recevoir du contenu obsolète.

L’utilisation de jetons de modification dans un scénario de mise en cache de fichier empêche la présence de contenu obsolète dans le cache. L’exemple d’application montre une implémentation de l’approche.

L’exemple utilise GetFileContent pour :

  • Retourner le contenu du fichier.
  • Implémentez un algorithme de nouvelle tentative avec interruption exponentielle pour couvrir les cas où un problème d’accès au fichier retarde temporairement la lecture du contenu du fichier.

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

Un FileService est créé pour gérer les recherches des fichiers mis en cache. L’appel de méthode GetFileContent du service tente d’obtenir le contenu du fichier à partir du cache en mémoire et de le renvoyer à l’appelant (Services/FileService.cs).

Si le contenu en cache n’est pas trouvé avec la clé du cache, les actions suivantes sont effectuées :

  1. Le contenu du fichier est obtenu à l’aide de GetFileContent.
  2. Un jeton de modification est obtenu auprès du fournisseur du fichier avec IFileProviders.Watch. Le rappel du jeton est déclenché quand le fichier est modifié.
  3. Le contenu du fichier est mis en cache avec une période d’expiration décalée. Le jeton de modification est attaché avec MemoryCacheEntryExtensions.AddExpirationToken pour supprimer l’entrée du cache si le fichier change alors qu’il est mis en cache.

Dans l’exemple suivant, des fichiers sont stockés à la racine du contenu de l’application. IHostingEnvironment.ContentRootFileProvider est utilisé pour obtenir un IFileProvider pointant vers ContentRootPath de l’application. filePath est obtenue avec 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 est inscrit dans le conteneur de service, ainsi que le service de mise en cache en mémoire.

Dans Startup.ConfigureServices :

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

Le modèle de page charge le contenu du fichier en utilisant le service.

Dans la méthode de OnGet la page Index (Pages/Index.cshtml.cs) :

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

Classe CompositeChangeToken

Pour représenter une ou plusieurs instances de IChangeToken dans un même objet, utilisez 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 sur le jeton composite indique true si l’élément HasChanged d’un jeton représenté est true. ActiveChangeCallbacks sur le jeton composite indique true si l’élément ActiveChangeCallbacks d’un jeton représenté est true. Si plusieurs modifications simultanées se produisent, le rappel de modification composite n’est appelé qu’une fois.

Ressources supplémentaires