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:
- Principio de Segregación de Interfaces o Interface Segregation Principle (ISP).
- Principio de Separación de Intereses o Separation of Concerns.
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:
- Debe ser no abstracta.
- Debe contener propiedades públicas de lectura y escritura. Los campos no se enlazan.
Enlace
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:
{
"MongoDbSettings": {
"Host": "localhost",
"Port": 27017
},
"RabbitMQSettings": {
"Host": "localhost"
}
}
Con el objetivo de obtener la sección MongoDbSettings
, se creara la siguiente clase de opciones.
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
:
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}");
}
}
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
:
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:
ConfigurationBinder.Get<MongoDbSettings>
enlaza y devuelve una instancia deMongoDbSettings
. Por este motivo, puede ser más conveniente usarConfigurationBinder.Get<T>
queConfigurationBinder.Bind
.- El parámetro
key
es el nombre de la sección de configuración que se va a buscar. No tiene que coincidir con el nombre del tipo que la representa. El operadornameof
se utiliza por convención cuando el nombre de la sección coincide con el de la clase de opciones.
Host: localhost
Puerto: 27017
Interfaz IOptions
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>
:
- No admite la lectura de los datos de configuración una vez iniciada la aplicación.
- No admite opciones con nombre.
- Se registra como
Singleton
y se puede insertar en cualquier duración del servicio.
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.
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:
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}");
}
}
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.
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
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:
IOptions<TOptions>
: crea la instancia una vez o cuando se necesita por primera vez. Siempre contiene la configuración desde que se creó la instancia del objeto por primera vez.IOptionsSnapshot<TOptions>
: crea la instancia cuando es necesario o cuando la configuración ha cambiado desde la última que se creó.
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.
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
:
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}");
}
}
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.
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
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:
IOptionsSnapshot
: es un servicio con ámbito y proporciona una instantánea de las opciones en el momento en que se construye el objeto. Las instantáneas de opciones están diseñadas para usarlas con dependencias transitorias y con ámbito.IOptionsMonitor
: es un servicio singleton que recupera los valores de las opciones actuales en cualquier momento, lo que resulta especialmente útil en las dependencias singleton.
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
:
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}");
}
}
Host: localhost
Puerto: 27017
Artículos Relacionados
- Acceso a la Configuración de ASP.NET Core Mediante IConfiguration.
- Validación de la Configuración en ASP.NET Core Mediante el Patrón de Opciones.
- Configuración Posterior en ASP.NET Core Mediante el Patrón de Opciones.
Recursos Adicionales
- Patrón de opciones en .NET.
- Clase ConfigurationBinder.
- Código Fuente de la Clase ConfigurationBinder.
- Interfaz IOptions<TOptions>.
- Código Fuente de la Interfaz IOptions<TOptions>.
- Interfaz IOptionsSnapshot<TOptions>.
- Código Fuente de la Interfaz IOptionsSnapshot<TOptions>.
- Interfaz IOptionsMonitor<TOptions>.
- Código Fuente de la Interfaz IOptionsMonitor<TOptions>.