Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 25 de febrero de 2025
Una lupa examinando de cerca opciones de configuración

Si lleváis algunos años en esto, seguro recordaréis que en ASP.NET "clásico", los settings de las aplicaciones eran valores inmutables. Se guardaban habitualmente en el célebre archivo Web.config, y cualquier cambio a este archivo implicaba reiniciar la aplicación para que los cambios tuvieran efecto.

En ASP.NET Core, los settings son mucho más flexibles: se pueden cargar desde diferentes orígenes, pueden almacenarse en distintos formatos, y es posible modificarlos en caliente sin necesidad de reiniciar la aplicación. Pero además, ofrecen otra capacidad que es interesante conocer: podemos detectar y reaccionar a cambios en los settings en tiempo real, es decir, en el mismo momento en que se producen.

Para ello, básicamente tendremos que bindear una clase personalizada a la sección de settings que nos interese, y luego utilizar la interfaz IOptionsMonitor, para registrar un manejador personalizado que se ejecute cada vez que se produzca un cambio en los valores de configuración que estamos monitorizando.

En este post vamos a ver cómo hacerlo, paso a paso.

1. Creamos una clase para los settings

Imaginemos que queremos implementar cualquier tipo de lógica personalizada cuando se produzcan cambios en la sección de settings MySettings, que tiene la siguiente estructura en el archivo appsettings.json:

{
    ...
    "Application": {
        "CompanyName": "Acme Corp.",
        "License": "ASD89DNC83HD8HJ",
        "ShowBetaFeatures": false
    },
    ...
}

Lo primero que necesitamos para poder detectar los cambios es crear una clase que represente los datos de esta sección. Podría ser la siguiente:

public record ApplicationSettings
{
    public required string CompanyName { get; init; }
    public bool ShowBetaFeatures { get; init; }
}

Fijaos que la clase puede definir, o no, los campos presentes en la sección de los settings. Si no definimos un campo, como ocurre con License en el ejemplo anterior, simplemente no obtendremos más adelante el valor actualizado del mismo.

2. Enlazamos la clase a la sección de settings

Para enlazar la clase ApplicationSettings a la sección de settings Application, necesitamos registrarla en el contenedor de dependencias de ASP.NET Core. Para ello, podemos hacer uso del método Configure<TOptions> de la interfaz IServiceCollection en el código de inicialización de la aplicación, en Program.cs, como puede verse a continuación:

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ApplicationSettings>(
    builder.Configuration.GetSection("Application")
);
...

Al insertar estas líneas en la configuración, ya podremos acceder de forma tipada a la sección de la configuración que nos interesa a través de instancias de IOptions<ApplicationSettings> o IOptionsSnapshot<ApplicationSettings> que podemos solicitar al inyector.

Por ejemplo, en el siguiente endpoint vemos cómo usar IOptions<T> para acceder a los valores de la sección Application:

app.MapGet("/", (IOptions<ApplicationSettings> settings) => $"{settings.Value.CompanyName}");

Al usar IOptions<T>, los valores de configuración no se actualizan automáticamente cuando se modifican en tiempo de ejecución, por lo que siempre obtendremos los valores que se cargaron en el arranque de la aplicación. Si queremos obtener los datos actualizados, necesitamos usar IOptionsSnapshot<T>, como en el siguiente ejemplo:

app.MapGet("/", (IOptionsSnapshot<ApplicationSettings> settings) 
    => $"{settings.Value.CompanyName}");

3. Registramos el monitor de cambios

Para detectar y reaccionar a cambios en los settings en tiempo real, es decir, ejecutar código personalizado cuando sean modificados, necesitamos acceder a una instancia de IOptionsMonitor<T>. Este servicio singleton que nos permite registrar handlers o manejadores que se ejecutarán cada vez que el framework detecte un cambio en valores de configuración.

Esto podemos hacerlo en el mismo código de inicialización, reclamando una instancia de IOptionsMonitor<T> al proveedor de servicios, y usando su método OnChange() para especificar el código a ejecutar cuando se produzcan cambios en la configuración:

...
var app = builder.Build();
var settingsMonitor = app.Services.GetRequiredService<IOptionsMonitor<ApplicationSettings>>();
settingsMonitor.OnChange(newValues =>
{
    // TODO: Reaccionar a los cambios
    app.Logger.LogInformation(
        $"New values: {newValues.CompanyName}, {newValues.ShowBetaFeatures}"
    );
});
...

Como podéis ver, el delegado que pasamos a OnChange() recibe un parámetro de tipo T, que en este caso es ApplicationSettings, y contiene los nuevos valores de configuración. En este caso, simplemente estamos escribiendo un mensaje en el log, pero podríamos hacer cualquier otra cosa que necesitemos.

Pero me gustaría añadir unos detalles importantes en este punto.

Primero, que a diferencia de eventos nativos de .NET, en este caso no es necesario liberar o "desuscribir" el manejador cuando ya no lo necesitamos. ASP.NET Core se encarga de gestionar la vida útil de los manejadores de forma automática.

Segundo, el manejador que especifiquemos OnChange() se invocará cuando cualquier valor de la configuración cambie, no necesariamente los que nos interesan. Por tanto, puede ser que nuestro código se ejecute aunque no haya cambios en los valores que estamos monitorizando.

Y tercero, por cada cambio que hagamos en caliente al archivo de configuración, el código manejador se ejecutará dos veces. El motivo es algo oscuro (podéis leer la explicación aquí) y está relacionado con la forma en que funcionan los watchers de archivos.

Por tanto, debido a estos dos últimos puntos, cuando vayamos a usar este mecanismos sería interesante implementar algún tipo de control para evitar ejecutar el código si los valores realmente no han cambiado. Por ejemplo, podríamos hacer algo como lo siguiente, donde almacenamos una instancia de los valores actuales y comparamos con los nuevos valores antes de ejecutar el código:

...
var currentApplicationSettings = settingsMonitor.CurrentValue;
settingsMonitor.OnChange(newValues =>
{
    if (newValues != currentApplicationSettings)
    {
        currentApplicationSettings = newValues;
        app.Logger.LogInformation(
            "Application settings updated" +
            $"{newValues.CompanyName}, {newValues.ShowBetaFeatures}"
        );
    }
    else
    {
        app.Logger.LogInformation("Application settings NOT updated, ignoring...");
    }
});
...

Ojo: la comparación entre la instancia almacenada y la que nos llega como parámetro la podemos implementar así porque se trata de un record, que implementa la comparación de igualdad por defecto comparando los valores de sus propiedades. Si usamos una clase normal, tendríamos que implementar la comparación de cada campo nosotros mismos.

4. El ejemplo completo

Como hemos ido viendo distintas piezas de la solución, aquí tenéis el código completo para que podáis probarlo en vuestro entorno:

using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ApplicationSettings>(
   builder.Configuration.GetSection("Application")
);

var app = builder.Build();


var settingsMonitor = app.Services.GetRequiredService<IOptionsMonitor<ApplicationSettings>>();
ApplicationSettings currentApplicationSettings = settingsMonitor.CurrentValue;
settingsMonitor.OnChange(newValues =>
{
    if (newValues != currentApplicationSettings)
    {
        currentApplicationSettings = newValues;
        app.Logger.LogInformation(
            "Application settings updated" +
            $"{newValues.CompanyName}, {newValues.ShowBetaFeatures}"
        );
    }
    else
    {
        app.Logger.LogInformation("Application settings NOT updated");
    }
});
app.Run();


public record ApplicationSettings
{
    public required string CompanyName { get; init; }
    public bool ShowBetaFeatures { get; init; }
}

¡Espero que os haya resultado útil!

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!