Detección de cambios con tokens de cambio en ASP.NET Core

Un token de cambio es un bloque de creación de bajo nivel y uso general que se usa para realizar el seguimiento de los cambios de estado.

Vea o descargue el código de ejemplo (cómo descargarlo)

Interfaz IChangeToken

IChangeToken propaga notificaciones que indican que se ha producido un cambio. IChangeToken reside en el espacio de nombres Microsoft.Extensions.Primitives. El paquete de NuGet Microsoft.Extensions.Primitives se proporciona implícitamente con las aplicaciones de ASP.NET Core.

IChangeToken tiene dos propiedades:

  • ActiveChangeCallbacks indica si el token genera devoluciones de llamada de manera proactiva. Si ActiveChangedCallbacks se establece en false, nunca se llama a una devolución de llamada y la aplicación debe sondear HasChanged en busca de cambios. También es posible que un token nunca se cancele si no se producen cambios o si se elimina o deshabilita el agente de escucha de cambios subyacente.
  • HasChanged recibe un valor que indica si se ha producido un cambio.

La interfaz IChangeToken incluye el método RegisterChangeCallback(Action<Object>, Object), que registra una devolución de llamada que se invoca cuando el token ha cambiado. HasChanged se debe establecer antes de que se invoque la devolución de llamada.

Clase ChangeToken

ChangeToken es una clase estática que se usa para propagar notificaciones que indican que se ha producido un cambio. ChangeToken reside en el espacio de nombres Microsoft.Extensions.Primitives. El paquete de NuGet Microsoft.Extensions.Primitives se proporciona implícitamente con las aplicaciones de ASP.NET Core.

El método ChangeToken.OnChange(Func<IChangeToken>, Action) registra un elemento Action al que se llama cada vez que cambia el token:

  • Func<IChangeToken> genera el token.
  • Se llama a Action cuando cambia el token.

La sobrecarga ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) toma un parámetro TState adicional que se pasa al consumidor de tokens Action.

OnChange devuelve un valor de IDisposable. Al llamar a Dispose se detiene la escucha del token de futuras modificaciones y se liberan sus recursos.

Ejemplos de uso de tokens de cambio en ASP.NET Core

Los tokens de cambio se usan en áreas principales de ASP.NET Core para supervisar los cambios en los objetos:

  • Para supervisar los cambios en los archivos, el método Watch de IFileProvider crea un IChangeToken para los archivos especificados o la carpeta que se va a supervisar.
  • Se pueden agregar tokens IChangeToken a las entradas de caché para desencadenar expulsiones de caché al producirse un cambio.
  • Para los cambios de TOptions, la implementación predeterminada OptionsMonitor<TOptions> de IOptionsMonitor<TOptions> tiene una sobrecarga que acepta una o varias instancias de IOptionsChangeTokenSource<TOptions>. Cada instancia devuelve un IChangeToken para registrar una devolución de llamada de notificación de cambio a fin de realizar el seguimiento de los cambios en las opciones.

Supervisión de los cambios de configuración

De forma predeterminada, las plantillas de ASP.NET Core usan archivos de configuración de JSON (appsettings.json, appsettings.Development.json y appsettings.Production.json) para cargar la configuración de la aplicación.

Estos archivos se configuran mediante el método de extensión AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) en ConfigurationBuilder que acepta un parámetro reloadOnChange. reloadOnChange indica si la configuración se debe recargar en los cambios de archivo. Esta configuración aparece en el método CreateDefaultBuilder de Host:

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

La configuración basada en archivo se presenta por medio de FileConfigurationSource. FileConfigurationSource use IFileProvider para supervisar los archivos.

PhysicalFileProvider proporciona IFileMonitor de manera predeterminada, que usa FileSystemWatcher para supervisar los cambios del archivo de configuración.

En la aplicación de ejemplo se muestran dos implementaciones para supervisar los cambios de configuración. Si se modifica cualquiera de los archivos appsettings, ambas implementaciones de supervisión de los archivos ejecutan código personalizado, y la aplicación de ejemplo escribe un mensaje en la consola.

El FileSystemWatcher de un archivo de configuración puede desencadenar varias devoluciones de llamada de token para un único cambio del archivo de configuración. Para garantizar que el código personalizado se ejecute solo una vez cuando se desencadenan varias devoluciones de llamada de token, la implementación del ejemplo comprueba los hashes de archivo. El ejemplo usa el algoritmo hash seguro 1. Se implementa un reintento con una interrupción 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 cambio de inicio simple

Registre una devolución de llamada de Action del consumidor de token para las notificaciones de cambio en el token de recarga de configuración.

En Startup.Configure:

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

config.GetReloadToken() proporciona el token. La devolución de llamada es el 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)");
    }
}

El state de la devolución de llamada se usa para pasar IWebHostEnvironment, que resulta útil para especificar el archivo de configuración appsettings correcto que se va a supervisar (por ejemplo, appsettings.Development.json cuando se está en el entorno de desarrollo). Se usa el hash de archivo para evitar que se ejecute varias veces la instrucción WriteConsole debido a varias devoluciones de llamada de token cuando el archivo de configuración solo ha cambiado una vez.

Este sistema se ejecuta siempre que la aplicación esté en ejecución y el usuario no lo puede deshabilitar.

Supervisión de los cambios de configuración como servicio

En el ejemplo se implementa lo siguiente:

  • La supervisión del token de inicio básico.
  • La supervisión como servicio.
  • Un mecanismo para habilitar y deshabilitar la supervisión.

El ejemplo establece una interfaz IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

El constructor de la clase implementada, ConfigurationMonitor, registra una devolución de llamada para las notificaciones de cambio:

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() proporciona el token. InvokeChanged es el método de devolución de llamada. El elemento state de esta instancia es una referencia a la instancia de IConfigurationMonitor que se usa para tener acceso al estado de supervisión. Se usan dos propiedades:

  • MonitoringEnabled: indica si la devolución de llamada debe ejecutar su código personalizado.
  • CurrentState: describe el estado de supervisión actual para su uso en la interfaz de usuario.

El método InvokeChanged es similar al enfoque anterior, excepto en que:

  • No ejecuta su código, a menos que MonitoringEnabled sea true.
  • Genera el state actual en su salida de 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}");
        }
    }
}

Una instancia de ConfigurationMonitor se registra como servicio en Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

En la página Index se ofrece al usuario el control sobre la supervisión de la configuración. La instancia de IConfigurationMonitor se inserta en IndexModel.

Pages/Index.cshtml.cs:

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

El monitor de configuración (_monitor) se usa para habilitar o deshabilitar la supervisión y establecer el estado actual de los comentarios de la interfaz de usuario:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Cuando se desencadena OnPostStartMonitoring, se habilita la supervisión y se borra el estado actual. Cuando se desencadena OnPostStopMonitoring, se deshabilita la supervisión y se establece el estado para reflejar que no se está realizando la supervisión.

Los botones de la interfaz de usuario habilitan y deshabilitan la supervisión.

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>

Supervisión de los cambios de archivos en caché

El contenido de los archivos se puede almacenar en caché en memoria mediante IMemoryCache. El almacenamiento en caché en memoria se describe en el tema Cache in-memory (Almacenamiento en caché en memoria). Sin realizar pasos adicionales, como la implementación que se describe a continuación, si los datos de origen cambian, se devuelven datos obsoletos (no actualizados) de la caché.

Por ejemplo, si no se tiene en cuenta el estado de un archivo de origen en caché cuando se renueva un período de vencimiento variable, se pueden crear datos de archivo en caché obsoletos. En cada solicitud de los datos se renueva el período de vencimiento variable, pero el archivo nunca se vuelve a cargar en la caché. Las características de la aplicación que usen el contenido en caché del archivo están sujetas a la posible recepción de contenido obsoleto.

El uso de tokens de cambio en un escenario de almacenamiento en caché de archivos evita la presencia de contenido de archivos obsoletos en la caché. En la aplicación de ejemplo se muestra una implementación del enfoque.

En el ejemplo se usa GetFileContent para:

  • Devolver el contenido del archivo.
  • Implemente un algoritmo de reintento con retroceso exponencial para cubrir los casos en los que un problema de acceso a archivos retrasa temporalmente la lectura del contenido del archivo.

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

Se crea un FileService para administrar las búsquedas de archivos en caché. La llamada al método GetFileContent del servicio intenta obtener el contenido de archivo de la caché en memoria y devolverlo al autor de la llamada (Services/FileService.cs).

Si el contenido en caché no se encuentra mediante la clave de caché, se realizan las acciones siguientes:

  1. El contenido del archivo se obtiene mediante GetFileContent.
  2. Se obtiene un token de cambio del proveedor de archivos con IFileProviders.Watch. La devolución de llamada del token se desencadena cuando se modifica el archivo.
  3. El contenido del archivo se almacena en caché con un período de vencimiento variable. El token de cambio se adjunta con MemoryCacheEntryExtensions.AddExpirationToken para expulsar la entrada de caché si el archivo cambia mientras está almacenado en caché.

En el ejemplo siguiente, los archivos se almacenan en la raíz del contenido de la aplicación. IWebHostEnvironment.ContentRootFileProvider se usa para obtener un IFileProvider que apunte a IWebHostEnvironment.ContentRootPath de la aplicación. La filePath se obtiene 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;
    }
}

El FileService se registra en el contenedor de servicios junto con el servicio de almacenamiento en caché.

En Startup.ConfigureServices:

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

El modelo de página carga el contenido del archivo mediante el servicio.

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

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

Clase CompositeChangeToken

Para representar una o varias instancias de IChangeToken en un solo objeto, use la clase 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
        });

En el token compuesto, HasChanged notifica true si algún token representado HasChanged es true. En el token compuesto, ActiveChangeCallbacks notifica true si algún token representado ActiveChangeCallbacks es true. Si se producen varios eventos de cambio simultáneos, la devolución de llamada de cambio compuesto se invoca una vez.

Un token de cambio es un bloque de creación de bajo nivel y uso general que se usa para realizar el seguimiento de los cambios de estado.

Vea o descargue el código de ejemplo (cómo descargarlo)

Interfaz IChangeToken

IChangeToken propaga notificaciones que indican que se ha producido un cambio. IChangeToken reside en el espacio de nombres Microsoft.Extensions.Primitives. En el caso de las aplicaciones que no usan el metapaquete Microsoft.AspNetCore.App, cree una referencia al paquete NuGet Microsoft.Extensions.Primitives.

IChangeToken tiene dos propiedades:

  • ActiveChangeCallbacks indica si el token genera devoluciones de llamada de manera proactiva. Si ActiveChangedCallbacks se establece en false, nunca se llama a una devolución de llamada y la aplicación debe sondear HasChanged en busca de cambios. También es posible que un token nunca se cancele si no se producen cambios o si se elimina o deshabilita el agente de escucha de cambios subyacente.
  • HasChanged recibe un valor que indica si se ha producido un cambio.

La interfaz IChangeToken incluye el método RegisterChangeCallback(Action<Object>, Object), que registra una devolución de llamada que se invoca cuando el token ha cambiado. HasChanged se debe establecer antes de que se invoque la devolución de llamada.

Clase ChangeToken

ChangeToken es una clase estática que se usa para propagar notificaciones que indican que se ha producido un cambio. ChangeToken reside en el espacio de nombres Microsoft.Extensions.Primitives. En el caso de las aplicaciones que no usan el metapaquete Microsoft.AspNetCore.App, cree una referencia al paquete NuGet Microsoft.Extensions.Primitives.

El método ChangeToken.OnChange(Func<IChangeToken>, Action) registra un elemento Action al que se llama cada vez que cambia el token:

  • Func<IChangeToken> genera el token.
  • Se llama a Action cuando cambia el token.

La sobrecarga ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) toma un parámetro TState adicional que se pasa al consumidor de tokens Action.

OnChange devuelve un valor de IDisposable. Al llamar a Dispose se detiene la escucha del token de futuras modificaciones y se liberan sus recursos.

Ejemplos de uso de tokens de cambio en ASP.NET Core

Los tokens de cambio se usan en áreas principales de ASP.NET Core para supervisar los cambios en los objetos:

  • Para supervisar los cambios en los archivos, el método Watch de IFileProvider crea un IChangeToken para los archivos especificados o la carpeta que se va a supervisar.
  • Se pueden agregar tokens IChangeToken a las entradas de caché para desencadenar expulsiones de caché al producirse un cambio.
  • Para los cambios de TOptions, la implementación predeterminada OptionsMonitor<TOptions> de IOptionsMonitor<TOptions> tiene una sobrecarga que acepta una o varias instancias de IOptionsChangeTokenSource<TOptions>. Cada instancia devuelve un IChangeToken para registrar una devolución de llamada de notificación de cambio a fin de realizar el seguimiento de los cambios en las opciones.

Supervisión de los cambios de configuración

De forma predeterminada, las plantillas de ASP.NET Core usan archivos de configuración de JSON (appsettings.json, appsettings.Development.json y appsettings.Production.json) para cargar la configuración de la aplicación.

Estos archivos se configuran mediante el método de extensión AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) en ConfigurationBuilder que acepta un parámetro reloadOnChange. reloadOnChange indica si la configuración se debe recargar en los cambios de archivo. Esta configuración aparece en el método CreateDefaultBuilder de WebHost:

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

La configuración basada en archivo se presenta por medio de FileConfigurationSource. FileConfigurationSource use IFileProvider para supervisar los archivos.

PhysicalFileProvider proporciona IFileMonitor de manera predeterminada, que usa FileSystemWatcher para supervisar los cambios del archivo de configuración.

En la aplicación de ejemplo se muestran dos implementaciones para supervisar los cambios de configuración. Si se modifica cualquiera de los archivos appsettings, ambas implementaciones de supervisión de los archivos ejecutan código personalizado, y la aplicación de ejemplo escribe un mensaje en la consola.

El FileSystemWatcher de un archivo de configuración puede desencadenar varias devoluciones de llamada de token para un único cambio del archivo de configuración. Para garantizar que el código personalizado se ejecute solo una vez cuando se desencadenan varias devoluciones de llamada de token, la implementación del ejemplo comprueba los hashes de archivo. El ejemplo usa el algoritmo hash seguro 1. Se implementa un reintento con una interrupción 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 cambio de inicio simple

Registre una devolución de llamada de Action del consumidor de token para las notificaciones de cambio en el token de recarga de configuración.

En Startup.Configure:

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

config.GetReloadToken() proporciona el token. La devolución de llamada es el 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)");
    }
}

El state de la devolución de llamada se usa para pasar IHostingEnvironment, que resulta útil para especificar el archivo de configuración appsettings correcto que se va a supervisar (por ejemplo, appsettings.Development.json cuando se está en el entorno de desarrollo). Se usa el hash de archivo para evitar que se ejecute varias veces la instrucción WriteConsole debido a varias devoluciones de llamada de token cuando el archivo de configuración solo ha cambiado una vez.

Este sistema se ejecuta siempre que la aplicación esté en ejecución y el usuario no lo puede deshabilitar.

Supervisión de los cambios de configuración como servicio

En el ejemplo se implementa lo siguiente:

  • La supervisión del token de inicio básico.
  • La supervisión como servicio.
  • Un mecanismo para habilitar y deshabilitar la supervisión.

El ejemplo establece una interfaz IConfigurationMonitor.

Extensions/ConfigurationMonitor.cs:

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

El constructor de la clase implementada, ConfigurationMonitor, registra una devolución de llamada para las notificaciones de cambio:

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() proporciona el token. InvokeChanged es el método de devolución de llamada. El elemento state de esta instancia es una referencia a la instancia de IConfigurationMonitor que se usa para tener acceso al estado de supervisión. Se usan dos propiedades:

  • MonitoringEnabled: indica si la devolución de llamada debe ejecutar su código personalizado.
  • CurrentState: describe el estado de supervisión actual para su uso en la interfaz de usuario.

El método InvokeChanged es similar al enfoque anterior, excepto en que:

  • No ejecuta su código, a menos que MonitoringEnabled sea true.
  • Genera el state actual en su salida de 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}");
        }
    }
}

Una instancia de ConfigurationMonitor se registra como servicio en Startup.ConfigureServices:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

En la página Index se ofrece al usuario el control sobre la supervisión de la configuración. La instancia de IConfigurationMonitor se inserta en IndexModel.

Pages/Index.cshtml.cs:

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

El monitor de configuración (_monitor) se usa para habilitar o deshabilitar la supervisión y establecer el estado actual de los comentarios de la interfaz de usuario:

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

    return RedirectToPage();
}

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

    return RedirectToPage();
}

Cuando se desencadena OnPostStartMonitoring, se habilita la supervisión y se borra el estado actual. Cuando se desencadena OnPostStopMonitoring, se deshabilita la supervisión y se establece el estado para reflejar que no se está realizando la supervisión.

Los botones de la interfaz de usuario habilitan y deshabilitan la supervisión.

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>

Supervisión de los cambios de archivos en caché

El contenido de los archivos se puede almacenar en caché en memoria mediante IMemoryCache. El almacenamiento en caché en memoria se describe en el tema Cache in-memory (Almacenamiento en caché en memoria). Sin realizar pasos adicionales, como la implementación que se describe a continuación, si los datos de origen cambian, se devuelven datos obsoletos (no actualizados) de la caché.

Por ejemplo, si no se tiene en cuenta el estado de un archivo de origen en caché cuando se renueva un período de vencimiento variable, se pueden crear datos de archivo en caché obsoletos. En cada solicitud de los datos se renueva el período de vencimiento variable, pero el archivo nunca se vuelve a cargar en la caché. Las características de la aplicación que usen el contenido en caché del archivo están sujetas a la posible recepción de contenido obsoleto.

El uso de tokens de cambio en un escenario de almacenamiento en caché de archivos evita la presencia de contenido de archivos obsoletos en la caché. En la aplicación de ejemplo se muestra una implementación del enfoque.

En el ejemplo se usa GetFileContent para:

  • Devolver el contenido del archivo.
  • Implemente un algoritmo de reintento con retroceso exponencial para cubrir los casos en los que un problema de acceso a archivos retrasa temporalmente la lectura del contenido del archivo.

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

Se crea un FileService para administrar las búsquedas de archivos en caché. La llamada al método GetFileContent del servicio intenta obtener el contenido de archivo de la caché en memoria y devolverlo al autor de la llamada (Services/FileService.cs).

Si el contenido en caché no se encuentra mediante la clave de caché, se realizan las acciones siguientes:

  1. El contenido del archivo se obtiene mediante GetFileContent.
  2. Se obtiene un token de cambio del proveedor de archivos con IFileProviders.Watch. La devolución de llamada del token se desencadena cuando se modifica el archivo.
  3. El contenido del archivo se almacena en caché con un período de vencimiento variable. El token de cambio se adjunta con MemoryCacheEntryExtensions.AddExpirationToken para expulsar la entrada de caché si el archivo cambia mientras está almacenado en caché.

En el ejemplo siguiente, los archivos se almacenan en la raíz del contenido de la aplicación. IHostingEnvironment.ContentRootFileProvider se usa para obtener un elemento IFileProvider que apunta a la ContentRootPath de la aplicación. La filePath se obtiene 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;
    }
}

El FileService se registra en el contenedor de servicios junto con el servicio de almacenamiento en caché.

En Startup.ConfigureServices:

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

El modelo de página carga el contenido del archivo mediante el servicio.

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

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

Clase CompositeChangeToken

Para representar una o varias instancias de IChangeToken en un solo objeto, use la clase 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
        });

En el token compuesto, HasChanged notifica true si algún token representado HasChanged es true. En el token compuesto, ActiveChangeCallbacks notifica true si algún token representado ActiveChangeCallbacks es true. Si se producen varios eventos de cambio simultáneos, la devolución de llamada de cambio compuesto se invoca una vez.

Recursos adicionales