Comparteix a través de


Guía sobre los patrones de opciones para creadores de bibliotecas de .NET

Con la ayuda de la inserción de dependencias, el registro de los servicios y sus configuraciones correspondientes pueden utilizar el patrón de opciones. El patrón de opciones permite a los consumidores de su biblioteca y a sus servicios requerir instancias de interfaces de opciones, donde TOptions es la clase de opciones. El consumo de opciones de configuración mediante objetos fuertemente tipados ayuda a garantizar una representación de valores coherente, habilita la validación con anotaciones de datos y elimina la carga de analizar manualmente los valores de cadena. Hay muchos proveedores de configuración a los que los consumidores pueden recurrir. Con estos proveedores, los consumidores pueden configurar su biblioteca de muchas maneras.

Como autor de bibliotecas de .NET, conocerá instrucciones generales sobre cómo exponer correctamente el patrón de opciones a los consumidores de la biblioteca. Hay varias maneras de lograr lo mismo, y varias consideraciones que se deben contemplar.

Convenciones de nomenclatura

Por convención, los métodos de extensión responsables del registro de servicios se denominan Add{Service}, donde {Service} es un nombre descriptivo y significativo. Los métodos de extensión Add{Service} son habituales en ASP.NET Core y .NET.

✔️ CONSIDERE nombres que eliminan la ambigüedad de los servicios respecto a otras ofertas.

❌ NO utilice nombres que ya formen parte del ecosistema de .NET de los paquetes oficiales de Microsoft.

✔️ CONSIDERE la posibilidad de asignar nombres a clases estáticas que expongan métodos de extensión como {Type}Extensions, donde {Type} es el tipo que se va a extender.

Guía sobre los espacios de nombres

Los paquetes de Microsoft utilizan el espacio de nombres Microsoft.Extensions.DependencyInjection para unificar el registro de varias ofertas de servicio.

✔️ CONSIDERE un espacio de nombres que identifique claramente la oferta del paquete.

❌ NO use el espacio de nombres Microsoft.Extensions.DependencyInjection para los paquetes no oficiales de Microsoft.

Sin parámetros

Si el servicio puede funcionar con una configuración mínima o no explícita, considere la posibilidad de usar un método de extensión sin parámetros.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Specify default option values
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

Parámetro IConfiguration

Cuando crea una biblioteca que expone muchas opciones a los consumidores, puede que desee considerar la posibilidad de requerir un método de extensión de parámetros IConfiguration. La instancia esperada IConfiguration debe tener como ámbito una sección con nombre de la configuración mediante la función IConfiguration.GetSection.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      IConfiguration namedConfigurationSection)
    {
        // Default library options are overridden
        // by bound configuration values.
        services.Configure<LibraryOptions>(namedConfigurationSection);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

Los consumidores de este patrón proporcionan la instancia con ámbito IConfiguration de la sección titulada:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(
    builder.Configuration.GetSection("LibraryOptions"));

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

La llamada a .AddMyLibraryService se realiza en el tipo IServiceCollection.

Como autor de la biblioteca, la especificación de valores predeterminados depende de usted.

Nota:

Es posible enlazar la configuración a una instancia de opciones. Sin embargo, existe el riesgo de que se produzcan conflictos de nombres, lo que provocará errores. Además, cuando se realiza un enlace manual de esta forma, se limita el consumo del patrón de opciones a una sola lectura. Los cambios en la configuración no se volverán a enlazar, ya que estos consumidores no podrán usar la interfaz IOptionsMonitor.

services.AddOptions<LibraryOptions>()
    .Configure<IConfiguration>(
        (options, configuration) =>
            configuration.GetSection("LibraryOptions").Bind(options));

En su lugar, debe usar el método de extensión BindConfiguration. Este método de extensión enlaza la configuración a la instancia de opciones y también registra un origen de token de cambio para la sección de configuración. Esto permite a los consumidores usar la interfaz IOptionsMonitor.

Parámetro de ruta de acceso de la sección de configuración

Es posible que los consumidores de la biblioteca quieran especificar la ruta de acceso de la sección de configuración para enlazar el tipo subyacente TOptions. En este escenario, se define un parámetro string en el método de extensión.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      string configSectionPath)
    {
        services.AddOptions<SupportOptions>()
            .BindConfiguration(configSectionPath)
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

En el ejemplo siguiente, se usa el paquete NuGet Microsoft.Extensions.Options.DataAnnotations para habilitar la validación de anotaciones de datos. La clase SupportOptions se define del modo siguiente:

using System.ComponentModel.DataAnnotations;

public sealed class SupportOptions
{
    [Url]
    public string? Url { get; set; }

    [Required, EmailAddress]
    public required string Email { get; set; }

    [Required, DataType(DataType.PhoneNumber)]
    public required string PhoneNumber { get; set; }
}

Imagine que se usa el siguiente archivo JSON appsettings.json:

{
    "Support": {
        "Url": "https://support.example.com",
        "Email": "help@support.example.com",
        "PhoneNumber": "+1(888)-SUPPORT"
    }
}

Parámetro Action<TOptions>

A los consumidores de la biblioteca les puede interesar proporcionar una expresión lambda que genere una instancia de la clase de opciones. En este escenario, se define un parámetro Action<LibraryOptions> en el método de extensión.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services,
        Action<LibraryOptions> configureOptions)
    {
        services.Configure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

Los consumidores de este patrón proporcionan una expresión lambda, o bien un delegado que cumple el parámetro Action<LibraryOptions>:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // User defined option values
    // options.SomePropertyValue = ...
});
                                                                        
using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Parámetro de la instancia de opciones

Es posible que los consumidores de la biblioteca prefieran proporcionar una instancia de opciones insertadas. En este escenario, se expone un método de extensión que adopta una instancia del objeto de opciones, LibraryOptions.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      LibraryOptions userOptions)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Overwrite default option values
                // with the user provided options.
                // options.SomeValue = userOptions.SomeValue;
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

Los consumidores de este patrón proporcionan una instancia de la clase LibraryOptions, que define los valores de propiedad deseados insertados:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(new LibraryOptions
{
    // Specify option values
    // SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Después de la configuración

Una vez que se han enlazado o especificado todos los valores de las opciones de configuración, está disponible la funcionalidad posterior a la configuración. Al exponer el mismo parámetro Action<TOptions> detallado anteriormente, podría optar por llamar a PostConfigure. La configuración posterior se ejecuta después de todas las llamadas a .Configure. Hay algunas razones por las que le interesaría plantearse el uso de PostConfigure:

  • Orden de ejecución: puede invalidar los valores de configuración establecidos en las llamadas .Configure.
  • Validación: le permite validar que se han establecido los valores predeterminados después de aplicar el resto de configuraciones.
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      Action<LibraryOptions> configureOptions)
    {
        services.PostConfigure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

En el código anterior, AddMyLibraryService:

Los consumidores de este patrón proporcionan una expresión lambda (o un delegado que satisface el parámetro Action<LibraryOptions>), del mismo modo que lo harían con el parámetro Action<TOptions> en un escenario que no es posterior a la configuración:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // Specify option values
    // options.SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Vea también