Patrón de opciones en .NET
El patrón de opciones usa clases para proporcionar acceso fuertemente tipado a grupos de configuraciones relacionadas. Cuando los valores de configuración están aislados por escenario en clases independientes, la aplicación se ajusta a dos principios de ingeniería de software importantes:
- El principio de segregación de interfaz (ISP) o encapsulación: los escenarios (clases) que dependen de valores de configuración dependen únicamente de los valores de configuración que usen.
- Separación de intereses: Los valores de configuración para distintos elementos de la aplicación no son dependientes entre sí ni están emparejados.
Las opciones también proporcionan un mecanismo para validar los datos de configuración. Para obtener más información, consulte la sección Opciones de validación.
Enlace de configuración jerárquica
La mejor manera de leer valores de configuración relacionados es usar el patrón de opciones. El patrón de opciones se puede aplicar mediante la interfaz IOptions<TOptions>, donde el parámetro de tipo genérico TOptions
está restringido a class
. IOptions<TOptions>
se puede proporcionar posteriormente mediante la inserción de dependencias. Para más información, vea Inserción de dependencias en .NET.
Por ejemplo, para leer los valores de configuración resaltados de un archivo appsettings.json :
{
"SecretKey": "Secret key value",
"TransientFaultHandlingOptions": {
"Enabled": true,
"AutoRetryDelay": "00:00:07"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Cree la siguiente clase TransientFaultHandlingOptions
:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
Al usar el patrón de opciones, una clase de opciones:
- Debe ser no abstracta con un constructor público sin parámetros.
- Debe contener propiedades de lectura y escritura para enlazar (los campos no están enlazados)
El código siguiente forma parte del archivo de C# Program.cs y:
- Llama a ConfigurationBinder.Bind para enlazar la clase
TransientFaultHandlingOptions
a la sección"TransientFaultHandlingOptions"
. - Muestra los datos de configuración.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Bind(options);
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
// <Output>
// Sample output:
En el código anterior, el archivo de configuración JSON tiene su sección "TransientFaultHandlingOptions"
enlazada a la instancia TransientFaultHandlingOptions
. Esto hidrata las propiedades de los objetos C# con los valores correspondientes de la configuración.
ConfigurationBinder.Get<T>
enlaza y devuelve el tipo especificado. Puede ser más conveniente usar ConfigurationBinder.Get<T>
que ConfigurationBinder.Bind
. En el código siguiente se muestra cómo puede usar ConfigurationBinder.Get<T>
con la clase TransientFaultHandlingOptions
:
var options =
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Get<TransientFaultHandlingOptions>();
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
En el código anterior, se usa ConfigurationBinder.Get<T>
para adquirir una instancia del objeto TransientFaultHandlingOptions
con sus valores de propiedad rellenados desde la configuración subyacente.
Importante
La clase ConfigurationBinder expone varias API, como .Bind(object instance)
y .Get<T>()
, que no están restringidas a class
. Al utilizar cualquiera de las interfaces de opciones, debe cumplir las restricciones de la clase de opciones mencionadas anteriormente.
Un enfoque alternativo a la hora de usar el patrón de opciones consiste en enlazar la sección "TransientFaultHandlingOptions"
y agregarla al contenedor del servicio de inserción de dependencias. En el siguiente código se agrega TransientFaultHandlingOptions
al contenedor de servicios con Configure y se enlaza a la configuración:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<TransientFaultHandlingOptions>(
builder.Configuration.GetSection(
key: nameof(TransientFaultHandlingOptions)));
En el ejemplo anterior, builder
es una instancia de HostApplicationBuilder.
Sugerencia
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. Por ejemplo, podría tener una sección denominada "FaultHandling"
y podría estar representada por la clase TransientFaultHandlingOptions
. En esta instancia, en su lugar, debe pasar "FaultHandling"
a la función GetSection. El operador nameof
se utiliza por comodidad cuando la sección con nombre coincide con el tipo al que corresponde.
A partir del código anterior, el siguiente código lee las opciones de posición:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
En el código anterior, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación no se leen. Para leer los cambios una vez iniciada la aplicación, use IOptionsSnapshot o IOptionsMonitor para supervisar los cambios a medida que se producen y reaccionar en consecuencia.
Interfaces de opciones
- No admite:
- Lectura de los datos de configuración una vez iniciada la aplicación.
- Opciones con nombre
- Se registra como Singleton y se puede insertar en cualquier duración del servicio.
- Es útil en escenarios en los que las opciones se deben volver a calcular en cada resolución de inserción, en duraciones con ámbito o transitorias. Para obtener más información, consulte el apartado Uso de IOptionsSnapshot para leer datos actualizados.
- Se registra como Con ámbito y, por tanto, no se puede insertar en un servicio Singleton.
- Admite opciones con nombre
- se usa para recuperar las opciones y administrar las notificaciones de las opciones para instancias de
TOptions
. - Se registra como Singleton y se puede insertar en cualquier duración del servicio.
- Es compatible con:
- Notificaciones de cambios
- Opciones con nombre
- Configuración que se puede recargar
- Invalidación de opciones de selección (IOptionsMonitorCache<TOptions>)
IOptionsFactory<TOptions> es responsable de crear nuevas instancias de opciones. Tiene un solo método Create. La implementación predeterminada toma todas las instancias registradas de IConfigureOptions<TOptions> y IPostConfigureOptions<TOptions>, y establece todas las configuraciones primero, seguidas de las configuraciones posteriores. Distingue entre IConfigureNamedOptions<TOptions> y IConfigureOptions<TOptions>, y solo llama a la interfaz adecuada.
IOptionsMonitorCache<TOptions> se usa por IOptionsMonitor<TOptions> para almacenar en caché las instancias de TOptions
. IOptionsMonitorCache<TOptions> invalida instancias de opciones en la supervisión para que se pueda volver a calcular el valor (TryRemove). Los valores se pueden introducir manualmente y mediante TryAdd. Se usa el método Clear cuando todas las instancias con nombre se deben volver a crear a petición.
IOptionsChangeTokenSource<TOptions> se usa para capturar el objeto IChangeToken que realiza un seguimiento de los cambios en la instancia de TOptions
subyacente. Para obtener más información sobre los primitivos de token de cambio, vea Notificaciones de cambios.
Ventajas de las interfaces de opciones
El uso de un tipo de contenedor genérico proporciona la capacidad de desacoplar la duración de la opción del contenedor de inserción de dependencias (DI). La interfaz IOptions<TOptions>.Value proporciona una capa de abstracción, incluidas las restricciones genéricas, en el tipo de opciones. Esto proporciona las siguientes ventajas:
- La evaluación de la
T
instancia de configuración se aplaza al acceso de IOptions<TOptions>.Value, en lugar de cuando se inserta. Esto es importante porque puede utilizar la opciónT
desde varios lugares y elegir la semántica de duración sin cambiar nada enT
. - Al registrar opciones de tipo
T
, no es necesario registrar explícitamente el tipoT
. Esto se ofrece por comodidad cuando se crea una biblioteca con valores predeterminados sencillos y no se desea obligar al autor de la llamada a registrar opciones en el contenedor de DI con una duración específica. - Desde la perspectiva de la API, permite restricciones en el tipo
T
(en este caso,T
está restringido a un tipo de referencia).
Uso de IOptionsSnapshot para leer datos actualizados
Al usar IOptionsSnapshot<TOptions>, las opciones se calculan una vez por solicitud al acceder y se almacenan en caché durante la vigencia de la solicitud. Los cambios en la configuración se leen tras iniciarse la aplicación al usar proveedores de configuración que admiten la lectura de valores de configuración actualizados.
La diferencia entre IOptionsMonitor
y IOptionsSnapshot
es que:
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.IOptionsSnapshot
es un servicio con ámbito y proporciona una instantánea de las opciones en el momento en que se construye el objetoIOptionsSnapshot<T>
. Las instantáneas de opciones están diseñadas para usarlas con dependencias transitorias y con ámbito.
El código siguiente usa IOptionsSnapshot<TOptions>.
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
El código siguiente registra una instancia de configuración en la que se enlaza TransientFaultHandlingOptions
:
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
En el código anterior, el método Configure<TOptions>
se usa para registrar una instancia de configuración con la que TOptions
se enlazará y actualiza las opciones cuando la configuración cambie.
IOptionsMonitor
El tipo IOptionsMonitor
admite notificaciones de cambio y habilita escenarios en los que es posible que la aplicación tenga que responder a los cambios de origen de configuración dinámicamente. Esto resulta útil cuando necesita reaccionar a los cambios en los datos de configuración una vez iniciada la aplicación. Las notificaciones de cambio solo se admiten para proveedores de configuración basados en el sistema de archivos, como los siguientes:
- Microsoft.Extensions.Configuration.Ini
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.KeyPerFile
- Microsoft.Extensions.Configuration.UserSecrets
- Microsoft.Extensions.Configuration.Xml
Para usar el monitor de opciones, los objetos de opciones se configuran de la misma manera desde una sección de configuración.
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
En el ejemplo siguiente se usa IOptionsMonitor<TOptions>:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
public void DisplayValues()
{
TransientFaultHandlingOptions options = monitor.CurrentValue;
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
En el código anterior, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.
Sugerencia
Algunos sistemas de archivos, como contenedores de Docker y recursos compartidos de red, no pueden enviar notificaciones de cambio de forma confiable. Al usar la interfaz IOptionsMonitor<TOptions> en estos entornos, establezca la variable de entorno DOTNET_USE_POLLING_FILE_WATCHER
en 1
o true
para sondear el sistema de archivos en busca de cambios. El intervalo en el que se sondean los cambios es cada cuatro segundos y no se puede configurar.
Para obtener más información sobre los contenedores de Docker, vea Incluir una aplicación de .NET Core en un contenedor.
Compatibilidad de opciones con nombre con IConfigureNamedOptions
Opciones con nombre:
- son útiles cuando varias secciones de configuración se enlazan a las mismas propiedades.
- Distinguen mayúsculas de minúsculas.
Fíjese en el siguiente archivo appsettings.json:
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
En lugar de crear dos clases para enlazar Features:Personalize
y Features:WeatherStation
, se usará la clase siguiente para cada sección:
public class Features
{
public const string Personalize = nameof(Personalize);
public const string WeatherStation = nameof(WeatherStation);
public bool Enabled { get; set; }
public string ApiKey { get; set; }
}
El siguiente código configura las opciones con nombre:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<Features>(
Features.Personalize,
builder.Configuration.GetSection("Features:Personalize"));
builder.Services.Configure<Features>(
Features.WeatherStation,
builder.Configuration.GetSection("Features:WeatherStation"));
En el código siguiente se muestran las opciones con nombre:
public sealed class Service
{
private readonly Features _personalizeFeature;
private readonly Features _weatherStationFeature;
public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
{
_personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
_weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
}
}
Todas las opciones son instancias con nombre. Las instancias de IConfigureOptions<TOptions> se usan para seleccionar como destino la instancia de Options.DefaultName
, que es string.Empty
. IConfigureNamedOptions<TOptions> también implementa IConfigureOptions<TOptions>. La implementación predeterminada de IOptionsFactory<TOptions> tiene lógica para usar cada una de forma adecuada. La opción con nombre null
se usa para seleccionar como destino todas las instancias con nombre, en lugar de una instancia con nombre determinada. ConfigureAll y PostConfigureAll usan esta convención.
API OptionsBuilder
OptionsBuilder<TOptions> se usa para configurar instancias TOptions
. OptionsBuilder
simplifica la creación de opciones con nombre, ya que es un único parámetro para la llamada AddOptions<TOptions>(string optionsName)
inicial en lugar de aparecer en todas las llamadas posteriores. La validación de opciones y las sobrecargas ConfigureOptions
que aceptan las dependencias de servicio solo están disponibles mediante OptionsBuilder
.
OptionsBuilder
se usa en la sección Opciones de validación.
Uso de servicios de DI para configurar opciones
Al configurar opciones, puede usar la inserción de dependencias para acceder a los servicios registrados y usarlos para configurar las opciones. Esto resulta útil cuando necesita acceder a los servicios para configurar las opciones. Hay dos formas de acceder a los servicios desde DI durante la configuración de opciones:
Si se pasa un delegado de configuración a Configure en OptionsBuilder<TOptions>.
OptionsBuilder<TOptions>
proporciona sobrecargas de Configure que permiten usar hasta cinco servicios para configurar las opciones:builder.Services .AddOptions<MyOptions>("optionalName") .Configure<ExampleService, ScopedService, MonitorService>( (options, es, ss, ms) => options.Property = DoSomethingWith(es, ss, ms));
Si se crea un tipo que implementa IConfigureOptions<TOptions> o IConfigureNamedOptions<TOptions>, y se registra como un servicio.
Se recomienda pasar un delegado de configuración a Configure, ya que la creación de un servicio es más complicada. La creación de un tipo es equivalente a lo que el marco hace cuando se llama a Configure. La llamada a Configure registra una interfaz IConfigureNamedOptions<TOptions> genérica y transitoria, con un constructor que acepta los tipos de servicio genéricos especificados.
Opciones de validación
Opciones de validación permite que se validen los valores de opción.
Fíjese en el siguiente archivo appsettings.json:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from Awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
La siguiente clase se enlaza a la sección de configuración "MyCustomSettingsSection"
y aplica un par de reglas DataAnnotations
:
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int Scale { get; set; }
[Required]
public required int VerbosityLevel { get; set; }
}
En la clase SettingsOptions
anterior, la propiedad ConfigurationSectionName
contiene el nombre de la sección de configuración a la que se enlazará. En este escenario, el objeto options proporciona el nombre de su sección de configuración.
Sugerencia
El nombre de la sección de configuración es independiente del objeto de configuración al que se enlaza. Es decir, una sección de configuración denominada "FooBarOptions"
se puede enlazar a un objeto de opciones denominado ZedOptions
. Aunque puede ser habitual nombrarlos igual, no es necesario y puede provocar conflictos de nombres.
El código siguiente:
- Llama a AddOptions para obtener un elemento OptionsBuilder<TOptions> que enlaza a la clase
SettingsOptions
. - Llama a ValidateDataAnnotations para habilitar la validación con
DataAnnotations
.
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations();
El método de extensión ValidateDataAnnotations
se define en el paquete NuGet Microsoft.Extensions.Options.DataAnnotations.
En el código siguiente se muestran los valores de configuración o errores de validación de informes:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ValidationService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<SettingsOptions> _config;
public ValidationService(
ILogger<ValidationService> logger,
IOptions<SettingsOptions> config)
{
_config = config;
_logger = logger;
try
{
SettingsOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures)
{
_logger.LogError("Validation error: {FailureMessage}", failure);
}
}
}
}
El código siguiente aplica una regla de validación más compleja mediante un delegado:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
La validación se produce en tiempo de ejecución, pero puede configurarla para que se produzca al iniciarse encadenando una llamada a ValidateOnStart
:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.")
.ValidateOnStart();
A partir de .NET 8, puede usar una API alternativa, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), que habilita la validación al iniciarse para un tipo de opciones específico:
builder.Services
.AddOptionsWithValidateOnStart<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
IValidateOptions
para llevar a cabo una validación compleja
La siguiente clase implementa IValidateOptions<TOptions>:
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
sealed partial class ValidateSettingsOptions(
IConfiguration config)
: IValidateOptions<SettingsOptions>
{
public SettingsOptions? Settings { get; private set; } =
config.GetSection(SettingsOptions.ConfigurationSectionName)
.Get<SettingsOptions>();
public ValidateOptionsResult Validate(string? name, SettingsOptions options)
{
StringBuilder? failure = null;
if (!ValidationRegex().IsMatch(options.SiteTitle))
{
(failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
}
if (options.Scale is < 0 or > 1_000)
{
(failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
}
if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
{
(failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
}
return failure is not null
? ValidateOptionsResult.Fail(failure.ToString())
: ValidateOptionsResult.Success;
}
[GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
private static partial Regex ValidationRegex();
}
IValidateOptions
permite mover el código de validación dentro de una clase.
Nota
Este código de ejemplo se basa en el paquete NuGet Microsoft.Extensions.Configuration.Json.
Con el código anterior, la validación se habilita cuando se configuran servicios con el código siguiente:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<SettingsOptions>(
builder.Configuration.GetSection(
SettingsOptions.ConfigurationSectionName));
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton
<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());
Configuración posterior de las opciones
Establezca la configuración posterior con IPostConfigureOptions<TOptions>. Las tareas posteriores a la configuración se ejecutan una vez realizada toda la configuración de IConfigureOptions<TOptions>, y pueden resultar útiles en los escenarios en los que es necesario invalidar la configuración:
builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
PostConfigure está disponible para configurar posteriormente las opciones con nombre:
builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Use PostConfigureAll para configurar posteriormente todas las instancias de configuración:
builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});