ASP.NET CORE

Acceso a la Configuración de ASP.NET Core Mediante el Patrón de Opciones

ASP.NET Core promueve que todo acceso a la configuración se realice mediante la implementación del Patrón de Opciones, el cual utiliza clases para proporcionar acceso fuertemente tipado a grupos de configuraciones relacionadas. Este enfoque limpio permite evitar errores tipográficos y problemas de conversión, contrario al uso de la abstracción de la interfaz IConfiguration.

El uso de clases u objetos fuertemente tipados, permite representar una pequeña colección de configuraciones de forma aislada e independiente, definiendo así una única función en la aplicación. De esta forma se implementan dos principios de ingeniería de software importantes:

El uso de clases también proporciona un mecanismo para validar los datos de configuración. Estas validaciones se pueden implementar mediante reglas de validadores de anotación de datos (DataAnnotations), o el uso de la interfaz IValidateOptions, en caso de requerir validaciones complejas.

Al momento de implementar el patrón de opciones, una clase de opciones debe cumplir los siguientes requerimientos:

  1. Debe ser no abstracta.
  2. Debe contener propiedades públicas de lectura y escritura. Los campos no se enlazan.

Enlace Subir

El sistema de configuración de ASP.NET Core incluye la clase ConfigurationBinder, que permite tomar una colección de valores de configuración y vincularlos a un objeto fuertemente tipado o clase. Este proceso de enlazamiento o vinculación es similar al concepto de deserialización de archivos JSON.

Para llevar a cabo el proceso de enlace, la clase ConfigurationBinder expone dos métodos: Bind y Get.

Bind

El método Bind Intenta enlazar la instancia de objeto especificada con los valores de configuración, mediante la comparación de los nombres de propiedad con las claves de configuración de forma recursiva.

Considere el siguiente archivo appsettings.json de ejemplo:

appsettings.json
{
   "MongoDbSettings": {
      "Host": "localhost",
      "Port": 27017
   },
   "RabbitMQSettings": {
      "Host": "localhost"
   }
}

Con el objetivo de obtener la sección MongoDbSettings, se creara la siguiente clase de opciones.

MongoDbSettings
public class MongoDbSettings
{
    public string Host { get; set; }
    public int Port { get; set; }
    public string ConnectionString => $"mongodb://{Host}:{Port}"; //Este campo no se enlazará.
}

El siguiente código de una Razor Page, enlazará la sección MongoDbSettings del archivo appsettings.json de ejemplo, con la clase de opciones MongoDbSettings mediante el método Bind:

Index.razor
public class IndexModel : PageModel
{
    private readonly IConfiguration _configuration;

    public IndexModel(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public ContentResult OnGet()
    {
        MongoDbSettings mongoDbSettings = new();

        _configuration.GetSection("MongoDbSettings").Bind(mongoDbSettings);

        return Content($"Host: {mongoDbSettings?.Host} \n" +
                       $"Puerto: {mongoDbSettings?.Port}");
    }
}
Resultado
Host: localhost 
Puerto: 27017

Get

El método Get Intenta enlazar la instancia de configuración a una nueva instancia de tipo T. Si esta sección de configuración tiene un valor, es el que se usará. De lo contrario, se enlaza mediante la comparación de los nombres de propiedad con las claves de configuración de forma recursiva.

El siguiente código de una Razor Page, enlazará la sección MongoDbSettings del archivo appsettings.json de ejemplo, con la clase de opciones MongoDbSettings mediante el método Get:

Index.razor
public class IndexModel : PageModel
{
    private readonly IConfiguration _configuration;

    public IndexModel(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public ContentResult OnGet()
    {
        MongoDbSettings? mongoDbSettings = _configuration.GetSection(nameof(MongoDbSettings)).Get<MongoDbSettings>();

        return Content($"Host: {mongoDbSettings?.Host} \n" +
                       $"Puerto: {mongoDbSettings.Port}");
    }
}

En el código anterior:

Resultado
Host: localhost 
Puerto: 27017

Interfaz IOptions Subir

IOptions<TOptions> es una interfaz simple con una única propiedad: Value, la misma que permite recuperar la instancia de la clase de opciones que representa la sección de configuración en tiempo de ejecución.

La interfaz IOptions<TOptions>:

Cumpliendo con el último requerimiento de que las clases de opciones deben configurarse como servicios antes de poder ser utilizadas. El siguiente código de la clase Program.cs, muestra cómo realizar el registro en el contenedor DI de la clase de opciones MongoDbSettings, que tiene el mismo nombre de la sección a recuperar del archivo appsettings.json de ejemplo.

Este último paso le permite inyectar sus clases de opciones mediante IOptions<TOptions> en donde lo requiera, brindando así acceso encapsulado y fuertemente tipado a sus valores de configuración.

Program.cs
builder.Services.Configure(builder.Configuration.GetSection(nameof(MongoDbSettings)));

A partir del código anterior, el siguiente código de una Razor Page, lee la sección de configuración MongoDbSettings del archivo appsettings.json enlazada a la clase de opciones del mismo nombre:

Index.razor
public class IndexModel : PageModel
{
    private readonly MongoDbSettings? _mongoDbSettings;

    public IndexModel(IOptions<MongoDbSettings> options)
    {
        _mongoDbSettings = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Host: {_mongoDbSettings?.Host} \n" +
                       $"Puerto: {_mongoDbSettings?.Port}");
    }
}
Resultado
Host: localhost 
Puerto: 27017

La vinculación de la clase de opciones T a ConfigurationSection ocurre cuando se solicita IOptions<TOptions> por primera vez. El objeto se registra en el contenedor DI como un singleton, por lo que se vincula solo una vez.

La interfaz IOptions<TOptions> tiene el inconveniente de que no existe un equivalente al parámetro reloadOnChange, que permite recargar las clases de opciones fuertemente tipadas. IConfiguration aún se recargará si edita sus archivos appsettings.json, pero no se propagará a las clases de opciones. Sin embargo, para estos casos ASP.NET Core expone la interfaz IOptionsSnapshot<TOptions>, la misma que se verá a continuación.

¡Importante!
¡IMPORTANTE!

Si olvida llamar a Configure e inyectar IOption en sus servicios no verá ningún error, sin embargo, la clase de opciones T no estará vinculada a ninguna sección, y solo tendrá valores predeterminados en sus propiedades.

Interfaz IOptionsSnapshot Subir

Tal como se vio anteriormente, el principal inconveniente de la interfaz IOptions<TOptions> es que la clase de opciones nunca cambia, incluso si modifica el archivo de configuración subyacente desde el cual se cargó, como por ejemplo un archivo appsettings.json.

Esta situación no siempre es un problema (de todos modos, generalmente no se debería modificar archivos en servidores de producción en vivo), pero si necesita de esta funcionalidad, puede usar la interfaz IOptionsSnapshot<TOptions>. Conceptualmente, IOptionsSnaphot<T> es idéntica a IOptions<TOptions> en que es una representación fuertemente tipada de una sección de configuración. La diferencia es cuándo y con qué frecuencia se crean los objetos de opciones cuando se utilizan:

¡Importante!
¡IMPORTANTE!

IOptionsSnapshot está registrado como un servicio con alcance o scoped service, por lo que no puede ser inyectado en servicios singleton; si lo hace, tendrá una dependencia cautiva, lo que significa que hace referencia a la configuración incorrecta de la duración de los servicios, donde un servicio de mayor duración mantiene una dependencia cautiva del servicio de menor duración.

IOptionsSnapshot<TOptions> se configura automáticamente para sus clases de opciones al mismo tiempo que IOptions<TOptions>, por lo que puede usarlo en sus servicios exactamente de la misma manera.

Program.cs
builder.Services.Configure(builder.Configuration.GetSection(nameof(MongoDbSettings)));

A partir del código anterior, el siguiente código de una Razor Page, lee la sección de configuración MongoDbSettings del archivo appsettings.json enlazada a la clase de opciones del mismo nombre, y permite recargar los valores de configuración si han cambiado mediante la propiedad Value:

Index.razor
public class IndexModel : PageModel
{
    private readonly MongoDbSettings? _mongoDbSettings;

    public IndexModel(IOptionsSnapshot<MongoDbSettings> optionsSnapshot)
    {
        _mongoDbSettings = optionsSnapshot.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Host: {_mongoDbSettings?.Host} \n" +
                       $"Puerto: {_mongoDbSettings?.Port}");
    }
}
Resultado
Host: localhost 
Puerto: 27017

Puesto que IOptionsSnapshot<MongoDbSettings> está registrado como un servicio de ámbito, se recrea en cada solicitud. Si edita el archivo de configuración y hace que IConfiguration se vuelva a cargar, IOptionsSnapshot<MongoDbSettings> muestra los nuevos valores en la siguiente solicitud. Se crea un nuevo objeto MongoDbSettings con los nuevos valores de configuración y se utiliza para todas las solicitudes DI futuras.

¡Importante!
¡IMPORTANTE!

Tenga en cuenta que usar la interfaz IOptionsSnapshot para recargar la configuración no es un proceso “gratuito”. Está volviendo a vincular y reconfigurar la clase de opciones con cada solicitud, lo que puede tener implicaciones en el rendimiento. En la práctica, recargar la configuración no es común en producción, por lo que este enfoque debe ser usado en escenarios estrictamente necesarios.

Interfaz IOptionsMonitor Subir

IOptionsMonitor<TOptions> conceptualmente se comporta igual que IOptionsSnapshot<TOptions>, puesto que la clase de opciones se volverá a cargar cuando la configuración cambie. Sin embargo, tiene las siguientes diferencias:

Program.cs
builder.Services.Configure(builder.Configuration.GetSection(nameof(MongoDbSettings)));

A partir del código anterior, el siguiente código de una Razor Page, lee la sección de configuración MongoDbSettings del archivo appsettings.json enlazada a la clase de opciones del mismo nombre, y permite recargar los valores de configuración si han cambiado mediante la propiedad CurrentValue:

Index.razor
public class IndexModel : PageModel
{
    private readonly MongoDbSettings? _mongoDbSettings;

    public IndexModel(IOptionsSnapshot<MongoDbSettings> optionsSnapshot)
    {
        _mongoDbSettings = optionsSnapshot.CurrentValue;
    }

    public ContentResult OnGet()
    {
        return Content($"Host: {_mongoDbSettings?.Host} \n" +
                       $"Puerto: {_mongoDbSettings?.Port}");
    }
}
Resultado
Host: localhost 
Puerto: 27017

Artículos Relacionados

Recursos Adicionales