El Options Pattern es la forma de mapear la configuración a clases tipadas, en lugar de leer valores sueltos como cadenas de texto.
Esto nos da tipos reales, autocompletado, validación y código más fácil de probar. Que no es poca cosa, porque la configuración suele estar en todas partes (y cuando falla, falla con ganas).
Los valores de configuración no sensibles pueden vivir en appsettings.json: límites, nombres de secciones, opciones de logging, flags de comportamiento, etc. Para secretos reales, como contraseñas o claves privadas, mejor usar User Secrets, variables de entorno o un almacén de secretos.
El problema de las cadenas mágicas
Supongamos que tenemos esta configuración en appsettings.json para enviar correos:
{
"SmtpSettings": {
"Server": "smtp.gmail.com",
"Port": 587,
"SenderName": "No-Reply"
}
}La forma rápida de leer esto sería inyectar IConfiguration y acceder a las claves a mano:
var server = configuration["SmtpSettings:Server"];
var port = int.Parse(configuration["SmtpSettings:Port"]);Funciona, sí. Pero estamos pagando varios peajes:
- Errores de dedo: si escribimos
Severen lugar deServer, recibimosnully ya veremos dónde explota. - Sin tipos reales: todo llega como texto, así que toca convertir enteros, booleanos, fechas…
- Peor mantenimiento: las claves quedan desperdigadas por el código como pequeñas minas antipersona.
Usar IConfiguration directamente no está prohibido. Para leer un valor puntual en Program.cs puede estar bien. El problema viene cuando esa lectura se repite por servicios, controladores y clases varias.
La solución: Options Pattern
El Options Pattern consiste en crear una clase C# que represente una sección de configuración. Luego .NET se encarga de vincular los valores del JSON a esa clase.
Así pasamos de esto:
configuration["SmtpSettings:Server"]A esto:
options.Value.ServerMucho mejor. El compilador vuelve a estar de nuestro lado, que es donde nos gusta tenerlo.
Crear la clase de configuración
Creamos una clase simple (un POCO, vaya) cuyas propiedades coincidan con las claves del JSON:
public class SmtpOptions
{
public const string SectionName = "SmtpSettings";
public string Server { get; set; } = string.Empty;
public int Port { get; set; }
public string SenderName { get; set; } = string.Empty;
}Los nombres de las propiedades coinciden con los del JSON. La vinculación es case-insensitive, así que no distingue entre mayúsculas y minúsculas, aunque lo normal es mantener el mismo estilo para no volvernos locos.
Registrar las opciones en Program.cs
Ahora le decimos al contenedor de servicios qué sección del archivo debe usar para rellenar SmtpOptions.
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<SmtpOptions>(
builder.Configuration.GetSection(SmtpOptions.SectionName)
);
var app = builder.Build();También podríamos escribir la cadena "SmtpSettings" directamente, pero usar una constante evita repetir el nombre de la sección por todo el proyecto.
Inyectar y usar IOptions
Aquí hay un detalle importante: no inyectamos SmtpOptions directamente. Inyectamos IOptions<SmtpOptions>, que es el envoltorio que usa .NET para darnos acceso a esas opciones.
using Microsoft.Extensions.Options;
app.MapGet("/enviar-correo", (IOptions<SmtpOptions> options) =>
{
var config = options.Value;
return $"Conectando a {config.Server}:{config.Port} como {config.SenderName}";
});Ahora Port es un int, Server es una propiedad real y si renombramos algo, el compilador nos puede echar una mano.
IOptions vs IOptionsSnapshot vs IOptionsMonitor
Hay tres interfaces habituales para leer opciones. Se parecen mucho, pero no se usan para lo mismo.
| Interfaz | Ciclo de vida | Recarga cambios | Uso típico |
|---|---|---|---|
IOptions<T> | Singleton | No | Configuración estable durante la vida de la aplicación. |
IOptionsSnapshot<T> | Scoped | Sí, si el proveedor lo soporta | Opciones recalculadas una vez por petición. |
IOptionsMonitor<T> | Singleton | Sí | Servicios singleton que necesitan leer valores actualizados o reaccionar a cambios. |
Ojo con IOptionsSnapshot<T>.
Como es scoped, no podemos inyectarlo dentro de un servicio singleton. Si un singleton necesita configuración recargable, usamos IOptionsMonitor<T>.
En la mayoría de aplicaciones, IOptions<T> es suficiente. Si cambiamos archivos de configuración en caliente o tenemos servicios de larga vida, entonces ya tiene sentido mirar IOptionsSnapshot<T> o IOptionsMonitor<T>.
Validar la configuración
Hay un fallo bastante traicionero: si olvidamos poner Port en el JSON, su valor será 0, porque es el valor por defecto de un int.
Podemos validar las opciones al arrancar usando Data Annotations.
using System.ComponentModel.DataAnnotations;
public class SmtpOptions
{
public const string SectionName = "SmtpSettings";
[Required]
public string Server { get; set; } = string.Empty;
[Range(1, 65535)]
public int Port { get; set; }
public string SenderName { get; set; } = string.Empty;
}Y registramos las opciones con validación:
builder.Services.AddOptions<SmtpOptions>()
.Bind(builder.Configuration.GetSection(SmtpOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();Con ValidateOnStart(), la aplicación falla al arrancar si la configuración no es válida.
Si ValidateDataAnnotations() no aparece, revisa que el proyecto tenga disponible el paquete Microsoft.Extensions.Options.DataAnnotations.