Patrón de opciones en ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

Por Rick Anderson.

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:

  • Encapsulación:
    • las 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.

En este artículo se proporciona información sobre el patrón de opciones de ASP.NET Core. Para más información sobre el uso del patrón de opciones en las aplicaciones de consola, consulte Patrón de opciones en .NET.

Enlace de configuración jerárquica

La mejor manera de leer valores de configuración relacionados es usar el patrón de opciones. Por ejemplo, para leer los siguientes valores de configuración:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Cree la siguiente clase PositionOptions:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Una clase de opciones:

  • Debe ser no abstracto.
  • Tiene propiedades públicas de lectura y escritura del tipo que tienen los elementos correspondientes en la configuración enlazadas.
  • Tiene sus propiedades de lectura y escritura enlazadas a entradas coincidentes en la configuración.
  • No tienen sus campos enlazados. En el código anterior, Position no está enlazado. El campo Position se usa para que la cadena "Position" no se tenga que codificar de forma rígida en la aplicación al enlazar la clase a un proveedor de configuración.

El código siguiente:

  • Llama a ConfigurationBinder.Bind para enlazar la clase PositionOptions a la sección Position.
  • Muestra los datos de configuración de Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

Enlazar también permite la concreción de una clase abstracta. Tenga en cuenta el código siguiente que usa la clase abstracta SomethingWithAName:

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

El código siguiente muestra los valores de configuración de NameTitleOptions:

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

Las llamadas a Bind son menos estrictas que las llamadas a Get<>:

  • Bind permite la concreción de un abstracto.
  • Get<> tiene que crear una instancia propiamente dicha.

El patrón de opciones

Un enfoque alternativo a la hora de usar el patrón de opciones consiste en enlazar la sección Position y agregarla al contenedor del servicio de inserción de dependencias. En el siguiente código se agrega PositionOptions al contenedor de servicios con Configure y se enlaza a la configuración:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

A partir del código anterior, el siguiente código lee las opciones de posición:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

En el código anterior, los cambios en el archivo de configuración JSON producidos una vez iniciada la aplicación no se leen. Para leer los cambios una vez iniciada la aplicación, use IOptionsSnapshot.

Interfaces de opciones

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Los escenarios posteriores a la configuración permiten establecer o cambiar las opciones después de que finalice toda la configuración de IConfigureOptions<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.

Uso de IOptionsSnapshot para leer datos actualizados

Usar IOptionsSnapshot<TOptions>:

  • Cuando se accede a las opciones y se las almacena en caché durante la vigencia de la solicitud, se calculan una vez por solicitud.
  • Puede sufrir una pérdida de rendimiento significativa porque es un servicio con ámbito y se vuelve a calcular por solicitud. Para obtener más información, consulte Este problema de GitHub y Mejorar el rendimiento del enlace de configuración.
  • 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 objeto IOptionsSnapshot<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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

En el código anterior, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

IOptionsMonitor

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

En el ejemplo siguiente se usa IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 :

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

En lugar de crear dos clases para enlazar TopItem:Month y TopItem:Year, se usará la clase siguiente para cada sección:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

El siguiente código configura las opciones con nombre:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

En el código siguiente se muestran las opciones con nombre:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Vea Uso de AddOptions para configurar un repositorio personalizado para obtener información sobre cómo agregar un repositorio personalizado.

Uso de servicios de DI para configurar opciones

Hay dos formas de acceder a los servicios desde la inserción de dependencias durante la configuración de opciones:

  • Pasar 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<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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, que tiene 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 :

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

La siguiente clase se enlaza a la sección de configuración "MyConfig" y aplica un par de reglas DataAnnotations:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

El código siguiente:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

El método de extensión ValidateDataAnnotations se define en el paquete NuGet Microsoft.Extensions.Options.DataAnnotations. En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, se hace referencia implícita a este paquete desde el marco compartido.

En el código siguiente se muestran los valores de configuración o los errores de validación:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

El código siguiente aplica una regla de validación más compleja mediante un delegado:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> y IValidatableObject

La siguiente clase implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions permite mover el código de validación fuera de Program.cs y dentro de una clase.

Con el código anterior, la validación se habilita en Program.cs con el código siguiente:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

La validación de opciones también admite IValidatableObject. Para realizar la validación de nivel de clase de una clase dentro de la propia clase:

ValidateOnStart

La validación de opciones se ejecuta la primera vez que se crea una implementación IOptions<TOptions>, IOptionsSnapshot<TOptions> o IOptionsMonitor<TOptions>. Para ejecutar la validación de opciones diligentemente, cuando se inicie la aplicación, llame a ValidateOnStart en Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Configuración posterior de las opciones

Establezca la configuración posterior con IPostConfigureOptions<TOptions>. La configuración posterior se ejecuta una vez completada toda la configuración de IConfigureOptions<TOptions>:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure está disponible para configurar posteriormente las opciones con nombre:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

Use PostConfigureAll para configurar posteriormente todas las instancias de configuración:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Opciones de acceso en Program.cs

Para acceder a IOptions<TOptions> o IOptionsMonitor<TOptions> en Program.cs, llame a GetRequiredService en WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Recursos adicionales

Por Kirk Larkin y Rick Anderson.

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:

  • Encapsulación:
    • las 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.

En este artículo se proporciona información sobre el patrón de opciones de ASP.NET Core. Para más información sobre el uso del patrón de opciones en las aplicaciones de consola, consulte Patrón de opciones en .NET.

Enlace de configuración jerárquica

La mejor manera de leer valores de configuración relacionados es usar el patrón de opciones. Por ejemplo, para leer los siguientes valores de configuración:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Cree la siguiente clase PositionOptions:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Una clase de opciones:

  • Debe ser no abstracta con un constructor público sin parámetros.
  • Todas las propiedades de lectura y escritura públicas del tipo están enlazadas.
  • Los campos no se enlazan. En el código anterior, Position no está enlazado. El campo Position se usa para que la cadena "Position" no se tenga que codificar de forma rígida en la aplicación al enlazar la clase a un proveedor de configuración.

El código siguiente:

  • Llama a ConfigurationBinder.Bind para enlazar la clase PositionOptions a la sección Position.
  • Muestra los datos de configuración de Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

Un enfoque alternativo a la hora de usar el patrón de opciones consiste en enlazar la sección Position y agregarla al contenedor del servicio de inserción de dependencias. En el siguiente código se agrega PositionOptions al contenedor de servicios con Configure y se enlaza a la configuración:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

A partir del código anterior, el siguiente código lee las opciones de posición:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

En el código anterior, los cambios en el archivo de configuración JSON producidos una vez iniciada la aplicación no se leen. Para leer los cambios una vez iniciada la aplicación, use IOptionsSnapshot.

Interfaces de opciones

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Los escenarios posteriores a la configuración permiten establecer o cambiar las opciones después de que finalice toda la configuración de IConfigureOptions<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.

Uso de IOptionsSnapshot para leer datos actualizados

Usar IOptionsSnapshot<TOptions>:

  • Cuando se accede a las opciones y se las almacena en caché durante la vigencia de la solicitud, se calculan una vez por solicitud.
  • Puede sufrir una pérdida de rendimiento significativa porque es un servicio con ámbito y se vuelve a calcular por solicitud. Para obtener más información, consulte Este problema de GitHub y Mejorar el rendimiento del enlace de configuración.
  • 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 objeto IOptionsSnapshot<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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

En el código anterior, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

IOptionsMonitor

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

En el ejemplo siguiente se usa IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 :

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

En lugar de crear dos clases para enlazar TopItem:Month y TopItem:Year, se usará la clase siguiente para cada sección:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

El siguiente código configura las opciones con nombre:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

En el código siguiente se muestran las opciones con nombre:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Vea Uso de AddOptions para configurar un repositorio personalizado para obtener información sobre cómo agregar un repositorio personalizado.

Uso de servicios de DI para configurar opciones

Hay dos formas de acceder a los servicios desde la inserción de dependencias durante la configuración de opciones:

  • Pasar 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<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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, que tiene 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 :

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

La siguiente clase se enlaza a la sección de configuración "MyConfig" y aplica un par de reglas DataAnnotations:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

El código siguiente:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

El método de extensión ValidateDataAnnotations se define en el paquete NuGet Microsoft.Extensions.Options.DataAnnotations. En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, se hace referencia implícita a este paquete desde el marco compartido.

En el código siguiente se muestran los valores de configuración o los errores de validación:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

El código siguiente aplica una regla de validación más compleja mediante un delegado:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> y IValidatableObject

La siguiente clase implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions permite mover el código de validación fuera de Program.cs y dentro de una clase.

Con el código anterior, la validación se habilita en Program.cs con el código siguiente:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

La validación de opciones también admite IValidatableObject. Para realizar la validación de nivel de clase de una clase dentro de la propia clase:

ValidateOnStart

La validación de opciones se ejecuta la primera vez que se crea una implementación IOptions<TOptions>, IOptionsSnapshot<TOptions> o IOptionsMonitor<TOptions>. Para ejecutar la validación de opciones diligentemente, cuando se inicie la aplicación, llame a ValidateOnStart en Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Configuración posterior de las opciones

Establezca la configuración posterior con IPostConfigureOptions<TOptions>. La configuración posterior se ejecuta una vez completada toda la configuración de IConfigureOptions<TOptions>:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure está disponible para configurar posteriormente las opciones con nombre:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

Use PostConfigureAll para configurar posteriormente todas las instancias de configuración:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Opciones de acceso en Program.cs

Para acceder a IOptions<TOptions> o IOptionsMonitor<TOptions> en Program.cs, llame a GetRequiredService en WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Recursos adicionales

Por Kirk Larkin y Rick Anderson.

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:

  • Encapsulación:
    • las 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.

En este tema se proporciona información sobre el patrón de opciones de ASP.NET Core. Para más información sobre el uso del patrón de opciones en las aplicaciones de consola, consulte Patrón de opciones en .NET.

Vea o descargue el código de ejemplo (cómo descargarlo)

Enlace de configuración jerárquica

La mejor manera de leer valores de configuración relacionados es usar el patrón de opciones. Por ejemplo, para leer los siguientes valores de configuración:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Cree la siguiente clase PositionOptions:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

Una clase de opciones:

  • Debe ser no abstracta con un constructor público sin parámetros.
  • Todas las propiedades de lectura y escritura públicas del tipo están enlazadas.
  • Los campos no se enlazan. En el código anterior, Position no está enlazado. La propiedad Position se usa para que la cadena "Position" no tenga que estar codificada de forma rígida en la aplicación al enlazar la clase a un proveedor de configuración.

El código siguiente:

  • Llama a ConfigurationBinder.Bind para enlazar la clase PositionOptions a la sección Position.
  • Muestra los datos de configuración de Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

Un enfoque alternativo a la hora de usar el patrón de opciones consiste en enlazar la sección Position y agregarla al contenedor del servicio de inserción de dependencias. En el siguiente código se agrega PositionOptions al contenedor de servicios con Configure y se enlaza a la configuración:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

A partir del código anterior, el siguiente código lee las opciones de posición:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

En el código anterior, los cambios en el archivo de configuración JSON producidos una vez iniciada la aplicación no se leen. Para leer los cambios una vez iniciada la aplicación, use IOptionsSnapshot.

Interfaces de opciones

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Los escenarios posteriores a la configuración permiten establecer o cambiar las opciones después de que finalice toda la configuración de IConfigureOptions<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.

Uso de IOptionsSnapshot para leer datos actualizados

Al usar IOptionsSnapshot<TOptions>, cuando se accede a las opciones y se las almacena en caché durante la vigencia de la solicitud, se calculan una vez por 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 objeto IOptionsSnapshot<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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

En el código anterior, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

IOptionsMonitor

El código siguiente registra una instancia de configuración en la que se enlaza MyOptions.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

En el ejemplo siguiente se usa IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

En el código anterior, de forma predeterminada, los cambios en el archivo de configuración de JSON producidos una vez iniciada la aplicación se leen.

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 :

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

En lugar de crear dos clases para enlazar TopItem:Month y TopItem:Year, se usará la clase siguiente para cada sección:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

El siguiente código configura las opciones con nombre:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

En el código siguiente se muestran las opciones con nombre:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Vea Uso de AddOptions para configurar un repositorio personalizado para obtener información sobre cómo agregar un repositorio personalizado.

Uso de servicios de DI para configurar opciones

Hay dos formas de acceder a los servicios desde la inserción de dependencias durante la configuración de opciones:

  • Pasar 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:

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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, que tiene 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 :

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

La siguiente clase se enlaza a la sección de configuración "MyConfig" y aplica un par de reglas DataAnnotations:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

El código siguiente:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

El método de extensión ValidateDataAnnotations se define en el paquete NuGet Microsoft.Extensions.Options.DataAnnotations. En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web, se hace referencia implícita a este paquete desde el marco compartido.

En el código siguiente se muestran los valores de configuración o los errores de validación:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

El código siguiente aplica una regla de validación más compleja mediante un delegado:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

IValidateOptions para la validación compleja

La siguiente clase implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions permite mover el código de validación fuera de StartUp y dentro de una clase.

Con el código anterior, la validación se habilita en Startup.ConfigureServices con el código siguiente:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

Configuración posterior de las opciones

Establezca la configuración posterior con IPostConfigureOptions<TOptions>. La configuración posterior se ejecuta una vez completada toda la configuración de IConfigureOptions<TOptions>:

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure está disponible para configurar posteriormente las opciones con nombre:

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Use PostConfigureAll para configurar posteriormente todas las instancias de configuración:

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Acceso a opciones durante el inicio

IOptions<TOptions> y IOptionsMonitor<TOptions> puede usarse en Startup.Configure, ya que los servicios se compilan antes de que se ejecute el método Configure.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

No use IOptions<TOptions> o IOptionsMonitor<TOptions> en Startup.ConfigureServices. Puede que exista un estado incoherente de opciones debido al orden de los registros de servicio.

Paquete NuGet Options.ConfigurationExtensions

Se hace referencia implícita al paquete Microsoft.Extensions.Options.ConfigurationExtensions en las aplicaciones de ASP.NET Core.