Detectar alterações com tokens de alteração no ASP.NET Core

Um token de alteração é um bloco de construção de uso geral e de baixo nível, usado para controlar as alterações de estado.

Exibir ou baixar código de exemplo (como baixar)

Interface IChangeToken

IChangeToken propaga notificações de que ocorreu uma alteração. IChangeToken reside no namespace Microsoft.Extensions.Primitives. O pacote NuGet Microsoft.Extensions.Primitives é fornecido implicitamente aos aplicativos ASP.NET Core.

IChangeToken tem duas propriedades:

  • ActiveChangeCallbacks indica se o token de forma proativa gera retornos de chamada. Se ActiveChangedCallbacks é definido como false, um retorno de chamada nunca é chamado e o aplicativo precisa sondar HasChanged em busca de alterações. Também é possível que um token nunca seja cancelado se não ocorrerem alterações ou o ouvinte de alteração subjacente for removido ou desabilitado.
  • HasChanged recebe um valor que indica se uma alteração ocorreu.

A interface IChangeToken inclui o método RegisterChangeCallback(Action<Object>, Object), que registra um retorno de chamada que é invocado quando o token é alterado. HasChanged precisa ser definido antes de o retorno de chamada ser invocado.

Classe ChangeToken

ChangeToken é uma classe estática usada para propagar notificações de que ocorreu uma alteração. ChangeToken reside no namespace Microsoft.Extensions.Primitives. O pacote NuGet Microsoft.Extensions.Primitives é fornecido implicitamente aos aplicativos ASP.NET Core.

O método ChangeToken.OnChange(Func<IChangeToken>, Action) registra um Action para chamar sempre que o token é alterado:

  • Func<IChangeToken> produz o token.
  • Action é chamado quando o token é alterado.

A sobrecarga ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) usa um parâmetro TState adicional que é passado para o consumidor de token Action.

OnChange retorna um IDisposable. A chamada Dispose interrompe a escuta do token de outras alterações e libera os recursos do token.

Usos de exemplo de tokens de alteração no ASP.NET Core

Os tokens de alteração são usados nas áreas proeminentes do ASP.NET Core para monitorar alterações em objetos:

  • Para monitorar as alterações em arquivos, o método Watch de IFileProvider cria um IChangeToken para os arquivos especificados ou para pasta a ser inspecionada.
  • Tokens IChangeToken podem ser adicionados a entradas de cache para disparar remoções do cache após as alterações.
  • Para as alterações de TOptions, a implementação OptionsMonitor<TOptions> padrão de IOptionsMonitor<TOptions> tem uma sobrecarga que aceita uma ou mais instâncias IOptionsChangeTokenSource<TOptions>. Cada instância retorna um IChangeToken para registrar um retorno de chamada de notificação de alteração para o controle de alterações de opções.

Monitorar as alterações de configuração

Por padrão, os modelos do ASP.NET Core usam JSarquivos de configuração ON (appsettings.json, appsettings.Development.json e appsettings.Production.json) para carregar as configurações do aplicativo.

Esses arquivos são configurados com o método de extensão AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) no ConfigurationBuilder que aceita um parâmetro reloadOnChange. reloadOnChange indica se a configuração deve ser recarregada após alterações de arquivo. Essa configuração é exibida no método de conveniência CreateDefaultBuilder de Host:

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

A configuração baseada em arquivo é representada por FileConfigurationSource. FileConfigurationSource usa IFileProvider para monitorar arquivos.

Por padrão, o IFileMonitor é fornecido por um PhysicalFileProvider, que usa FileSystemWatcher para monitorar as alterações do arquivo de configuração.

O aplicativo de exemplo demonstra duas implementações para monitorar as alterações de configuração. Se qualquer um dos arquivos appsettings forem alterados, ambas as implementações de monitoramento de arquivo executam código personalizado—o aplicativo de exemplo grava uma mensagem no console.

O FileSystemWatcher de um arquivo de configuração pode disparar vários retornos de chamada de token para uma única alteração de arquivo de configuração. Para garantir que o código personalizado é executado somente depois que os vários retornos de chamada de token são acionados, a implementação da amostra verifica os hashes do arquivo. O exemplo usa o hash de arquivo SHA1. Uma nova tentativa é implementada com uma retirada exponencial.

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

Token de alteração de inicialização simples

Registre um retorno de chamada Action do consumidor de token para notificações de alteração no token de recarregamento de configuração.

Em Startup.Configure:

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

config.GetReloadToken() fornece o token. O retorno de chamada é o método 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)");
    }
}

O state do retorno de chamada é usado para passar no IWebHostEnvironment, que é útil para especificar o arquivo de configuração appsettings correto para monitorar (por exemplo, appsettings.Development.json quando estiver no Ambiente de desenvolvimento). Hashes de arquivo são usados para impedir que a instrução WriteConsole seja executada várias vezes, devido a vários retornos de chamada de token quando o arquivo de configuração é alterado somente uma vez.

Esse sistema é executado, desde que o aplicativo esteja em execução e não possa ser desabilitado pelo usuário.

Monitorar alterações de configuração como um serviço

A amostra implementa:

  • Monitoramento de token de inicialização básica.
  • Monitoramento como serviço.
  • Um mecanismo para habilitar e desabilitar o monitoramento.

A amostra estabelece uma interface IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

O construtor da classe implementada, ConfigurationMonitor, registra um retorno de chamada para notificações de alteração:

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() fornece o token. InvokeChanged é o método de retorno de chamada. O state nesta instância é uma referência à instância IConfigurationMonitor que é usada para acessar o estado de monitoramento. Duas propriedades são usadas:

  • MonitoringEnabled: indica se o retorno de chamada deve executar seu código personalizado.
  • CurrentState: descreve o estado atual de monitoramento para uso na interface do usuário.

O método InvokeChanged é semelhante à abordagem anterior, exceto que ele:

  • Não executa o código, a menos que MonitoringEnabled seja true.
  • Gera saída do state atual em sua saída 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}");
        }
    }
}

Uma instância ConfigurationMonitor é registrada como um serviço em Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

A página Índice oferece ao usuário o controle sobre o monitoramento de configuração. A instância de IConfigurationMonitor é injetada no IndexModel.

Pages/Index.cshtml.cs:

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

O monitor de configuração (_monitor) é usado para habilitar ou desabilitar o monitoramento e definir o estado atual de comentários de interface do usuário:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quando OnPostStartMonitoring é disparado, o monitoramento é habilitado e o estado atual é desmarcado. Quando OnPostStopMonitoring é disparado, o monitoramento é desabilitado e o estado é definido para refletir que o monitoramento não está ocorrendo.

Os botões na interface do usuário habilitam e desabilitar o monitoramento.

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>

Monitor alterações de arquivos armazenados em cache

O conteúdo do arquivo pode ser armazenado em cache em memória usando IMemoryCache. O cache na memória é descrito no tópico Cache na memória. Sem realizar etapas adicionais, como a implementação descrita abaixo, dados obsoletos (desatualizados) são retornados de um cache se os dados de origem são alterados.

Por exemplo, não levando em conta o status de um arquivo de origem armazenado em cache durante a renovação de um período de expiração deslizante resulta em dados de arquivo de cache obsoletos. Cada solicitação de dados renova o período de expiração deslizante, mas o arquivo nunca é recarregado no cache. Os recursos do aplicativo que usam o conteúdo armazenado em cache do arquivo estão sujeitos ao possível recebimento de conteúdo obsoleto.

O uso de tokens de alteração em um cenário de cache de arquivo impede a presença do conteúdo de arquivo obsoleto no cache. O aplicativo de exemplo demonstra uma implementação da abordagem.

A amostra usa GetFileContent para:

  • Retornar o conteúdo do arquivo.
  • Implementar um algoritmo de nova tentativa com recuo exponencial para abranger os casos em que um problema de acesso ao arquivo atrasa temporariamente a leitura do conteúdo do arquivo.

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

Um FileService é criado para manipular pesquisas de arquivos armazenados em cache. A chamada de método GetFileContent do serviço tenta obter o conteúdo do arquivo do cache em memória e retorná-lo para o chamador (Services/FileService.cs).

Se o conteúdo armazenado em cache não é encontrado com a chave de cache, as seguintes ações são executadas:

  1. O conteúdo do arquivo é obtido com GetFileContent.
  2. Um token de alteração é obtido do provedor de arquivo com IFileProviders.Watch. O retorno de chamada do token é disparado quando o arquivo é modificado.
  3. O conteúdo do arquivo é armazenado em cache com um período de expiração deslizante. O token de alteração é anexado com MemoryCacheEntryExtensions.AddExpirationToken para remover a entrada do cache se o arquivo é alterado enquanto ele é armazenado em cache.

No exemplo a seguir, os arquivos são armazenados na raiz do conteúdo do aplicativo. IWebHostEnvironment.ContentRootFileProvider é usado para obter um IFileProvider apontando para o IWebHostEnvironment.ContentRootPath do aplicativo. O filePath é obtido com 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;
    }
}

O FileService é registrado no contêiner de serviço junto com o serviço de cache da memória.

Em Startup.ConfigureServices:

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

O modelo de página carrega o conteúdo do arquivo usando o serviço.

No método OnGet de página de índice (Pages/Index.cshtml.cs):

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

Classe CompositeChangeToken

Para representar uma ou mais instâncias de IChangeToken em um único objeto, use a 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 nos relatórios de token compostos true se um token representado HasChanged é true. ActiveChangeCallbacks nos relatórios de token compostos true se um token representado ActiveChangeCallbacks é true. Se ocorrerem vários eventos de alteração simultâneos, o retorno de chamada de alteração composto será invocado uma vez.

Um token de alteração é um bloco de construção de uso geral e de baixo nível, usado para controlar as alterações de estado.

Exibir ou baixar código de exemplo (como baixar)

Interface IChangeToken

IChangeToken propaga notificações de que ocorreu uma alteração. IChangeToken reside no namespace Microsoft.Extensions.Primitives. Para aplicativos que não usam o metapacote Microsoft.AspNetCore.All, crie uma referência de pacote para o pacote NuGet Microsoft.Extensions.Primitives.

IChangeToken tem duas propriedades:

  • ActiveChangeCallbacks indica se o token de forma proativa gera retornos de chamada. Se ActiveChangedCallbacks é definido como false, um retorno de chamada nunca é chamado e o aplicativo precisa sondar HasChanged em busca de alterações. Também é possível que um token nunca seja cancelado se não ocorrerem alterações ou o ouvinte de alteração subjacente for removido ou desabilitado.
  • HasChanged recebe um valor que indica se uma alteração ocorreu.

A interface IChangeToken inclui o método RegisterChangeCallback(Action<Object>, Object), que registra um retorno de chamada que é invocado quando o token é alterado. HasChanged precisa ser definido antes de o retorno de chamada ser invocado.

Classe ChangeToken

ChangeToken é uma classe estática usada para propagar notificações de que ocorreu uma alteração. ChangeToken reside no namespace Microsoft.Extensions.Primitives. Para aplicativos que não usam o metapacote Microsoft.AspNetCore.All, crie uma referência de pacote para o pacote NuGet Microsoft.Extensions.Primitives.

O método ChangeToken.OnChange(Func<IChangeToken>, Action) registra um Action para chamar sempre que o token é alterado:

  • Func<IChangeToken> produz o token.
  • Action é chamado quando o token é alterado.

A sobrecarga ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) usa um parâmetro TState adicional que é passado para o consumidor de token Action.

OnChange retorna um IDisposable. A chamada Dispose interrompe a escuta do token de outras alterações e libera os recursos do token.

Usos de exemplo de tokens de alteração no ASP.NET Core

Os tokens de alteração são usados nas áreas proeminentes do ASP.NET Core para monitorar alterações em objetos:

  • Para monitorar as alterações em arquivos, o método Watch de IFileProvider cria um IChangeToken para os arquivos especificados ou para pasta a ser inspecionada.
  • Tokens IChangeToken podem ser adicionados a entradas de cache para disparar remoções do cache após as alterações.
  • Para as alterações de TOptions, a implementação OptionsMonitor<TOptions> padrão de IOptionsMonitor<TOptions> tem uma sobrecarga que aceita uma ou mais instâncias IOptionsChangeTokenSource<TOptions>. Cada instância retorna um IChangeToken para registrar um retorno de chamada de notificação de alteração para o controle de alterações de opções.

Monitorar as alterações de configuração

Por padrão, os modelos do ASP.NET Core usam JSarquivos de configuração ON (appsettings.json, appsettings.Development.json e appsettings.Production.json) para carregar as configurações do aplicativo.

Esses arquivos são configurados com o método de extensão AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) no ConfigurationBuilder que aceita um parâmetro reloadOnChange. reloadOnChange indica se a configuração deve ser recarregada após alterações de arquivo. Essa configuração é exibida no método de conveniência CreateDefaultBuilder de WebHost:

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

A configuração baseada em arquivo é representada por FileConfigurationSource. FileConfigurationSource usa IFileProvider para monitorar arquivos.

Por padrão, o IFileMonitor é fornecido por um PhysicalFileProvider, que usa FileSystemWatcher para monitorar as alterações do arquivo de configuração.

O aplicativo de exemplo demonstra duas implementações para monitorar as alterações de configuração. Se qualquer um dos arquivos appsettings forem alterados, ambas as implementações de monitoramento de arquivo executam código personalizado—o aplicativo de exemplo grava uma mensagem no console.

O FileSystemWatcher de um arquivo de configuração pode disparar vários retornos de chamada de token para uma única alteração de arquivo de configuração. Para garantir que o código personalizado é executado somente depois que os vários retornos de chamada de token são acionados, a implementação da amostra verifica os hashes do arquivo. O exemplo usa o hash de arquivo SHA1. Uma nova tentativa é implementada com uma retirada exponencial.

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

Token de alteração de inicialização simples

Registre um retorno de chamada Action do consumidor de token para notificações de alteração no token de recarregamento de configuração.

Em Startup.Configure:

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

config.GetReloadToken() fornece o token. O retorno de chamada é o método 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)");
    }
}

O state do retorno de chamada é usado para passar no IHostingEnvironment, que é útil para especificar o arquivo de configuração appsettings correto para monitorar (por exemplo, appsettings.Development.json quando estiver no Ambiente de desenvolvimento). Hashes de arquivo são usados para impedir que a instrução WriteConsole seja executada várias vezes, devido a vários retornos de chamada de token quando o arquivo de configuração é alterado somente uma vez.

Esse sistema é executado, desde que o aplicativo esteja em execução e não possa ser desabilitado pelo usuário.

Monitorar alterações de configuração como um serviço

A amostra implementa:

  • Monitoramento de token de inicialização básica.
  • Monitoramento como serviço.
  • Um mecanismo para habilitar e desabilitar o monitoramento.

A amostra estabelece uma interface IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

O construtor da classe implementada, ConfigurationMonitor, registra um retorno de chamada para notificações de alteração:

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() fornece o token. InvokeChanged é o método de retorno de chamada. O state nesta instância é uma referência à instância IConfigurationMonitor que é usada para acessar o estado de monitoramento. Duas propriedades são usadas:

  • MonitoringEnabled: indica se o retorno de chamada deve executar seu código personalizado.
  • CurrentState: descreve o estado atual de monitoramento para uso na interface do usuário.

O método InvokeChanged é semelhante à abordagem anterior, exceto que ele:

  • Não executa o código, a menos que MonitoringEnabled seja true.
  • Gera saída do state atual em sua saída 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}");
        }
    }
}

Uma instância ConfigurationMonitor é registrada como um serviço em Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

A página Índice oferece ao usuário o controle sobre o monitoramento de configuração. A instância de IConfigurationMonitor é injetada no IndexModel.

Pages/Index.cshtml.cs:

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

O monitor de configuração (_monitor) é usado para habilitar ou desabilitar o monitoramento e definir o estado atual de comentários de interface do usuário:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Quando OnPostStartMonitoring é disparado, o monitoramento é habilitado e o estado atual é desmarcado. Quando OnPostStopMonitoring é disparado, o monitoramento é desabilitado e o estado é definido para refletir que o monitoramento não está ocorrendo.

Os botões na interface do usuário habilitam e desabilitar o monitoramento.

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>

Monitor alterações de arquivos armazenados em cache

O conteúdo do arquivo pode ser armazenado em cache em memória usando IMemoryCache. O cache na memória é descrito no tópico Cache na memória. Sem realizar etapas adicionais, como a implementação descrita abaixo, dados obsoletos (desatualizados) são retornados de um cache se os dados de origem são alterados.

Por exemplo, não levando em conta o status de um arquivo de origem armazenado em cache durante a renovação de um período de expiração deslizante resulta em dados de arquivo de cache obsoletos. Cada solicitação de dados renova o período de expiração deslizante, mas o arquivo nunca é recarregado no cache. Os recursos do aplicativo que usam o conteúdo armazenado em cache do arquivo estão sujeitos ao possível recebimento de conteúdo obsoleto.

O uso de tokens de alteração em um cenário de cache de arquivo impede a presença do conteúdo de arquivo obsoleto no cache. O aplicativo de exemplo demonstra uma implementação da abordagem.

A amostra usa GetFileContent para:

  • Retornar o conteúdo do arquivo.
  • Implementar um algoritmo de nova tentativa com recuo exponencial para abranger os casos em que um problema de acesso ao arquivo atrasa temporariamente a leitura do conteúdo do arquivo.

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

Um FileService é criado para manipular pesquisas de arquivos armazenados em cache. A chamada de método GetFileContent do serviço tenta obter o conteúdo do arquivo do cache em memória e retorná-lo para o chamador (Services/FileService.cs).

Se o conteúdo armazenado em cache não é encontrado com a chave de cache, as seguintes ações são executadas:

  1. O conteúdo do arquivo é obtido com GetFileContent.
  2. Um token de alteração é obtido do provedor de arquivo com IFileProviders.Watch. O retorno de chamada do token é disparado quando o arquivo é modificado.
  3. O conteúdo do arquivo é armazenado em cache com um período de expiração deslizante. O token de alteração é anexado com MemoryCacheEntryExtensions.AddExpirationToken para remover a entrada do cache se o arquivo é alterado enquanto ele é armazenado em cache.

No exemplo a seguir, os arquivos são armazenados na raiz do conteúdo do aplicativo. IHostingEnvironment.ContentRootFileProvider é usado para obter IFileProvider apontando para ContentRootPath do aplicativo. O filePath é obtido com 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;
    }
}

O FileService é registrado no contêiner de serviço junto com o serviço de cache da memória.

Em Startup.ConfigureServices:

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

O modelo de página carrega o conteúdo do arquivo usando o serviço.

No método OnGet de página de índice (Pages/Index.cshtml.cs):

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

Classe CompositeChangeToken

Para representar uma ou mais instâncias de IChangeToken em um único objeto, use a 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 nos relatórios de token compostos true se um token representado HasChanged é true. ActiveChangeCallbacks nos relatórios de token compostos true se um token representado ActiveChangeCallbacks é true. Se ocorrerem vários eventos de alteração simultâneos, o retorno de chamada de alteração composto será invocado uma vez.

Recursos adicionais