Diretrizes de padrão de opções para autores da biblioteca .NET

Com a ajuda da injeção de dependência, registrar seus serviços e suas configurações correspondentes pode usar o padrão de opções. O padrão de opções permite que os consumidores de sua biblioteca (e seus serviços) exijam instâncias de interfaces de opções em que TOptions é sua classe de opções. O consumo de opções de configuração por meio de objetos fortemente tipados ajuda a garantir uma representação de valor consistente, permite a validação com anotações de dados e evita a análise manual dos valores de cadeia de caracteres. Há muitos provedores de configuração para os consumidores de sua biblioteca usarem. Com esses provedores, os consumidores podem configurar sua biblioteca de várias maneiras.

Como um autor da biblioteca .NET, você aprenderá orientações gerais sobre como expor corretamente o padrão de opções aos consumidores de sua biblioteca. Há várias maneiras de alcançar a mesma coisa, e várias considerações a serem feitas.

Convenções de nomenclatura

Por convenção, os métodos de extensão responsáveis por registrar serviços são nomeados Add{Service}, em que {Service} é um nome significativo e descritivo. Os métodos de extensão Add{Service} são comuns no ASP.NET Core e no .NET.

✔️ CONSIDERE os nomes que diferenciem seu serviço de outras ofertas.

❌ NÃO use nomes que já fazem parte do ecossistema .NET de pacotes oficiais da Microsoft.

✔️ CONSIDERE a nomenclatura de classes estáticas que expõem métodos de extensão como {Type}Extensions, em que {Type} é o tipo que você está estendendo.

Diretrizes de namespace

Os pacotes da Microsoft usam o namespace Microsoft.Extensions.DependencyInjection para unificar o registro de várias ofertas de serviço.

✔️ CONSIDERE um namespace que identifique claramente sua oferta de pacote.

❌ NÃO use o namespace Microsoft.Extensions.DependencyInjection para pacotes não oficiais da Microsoft.

Construtor

Se o serviço puder trabalhar com configuração mínima ou não explícita, considere um método de extensão sem 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;
    }
}

No código anterior, o AddMyLibraryService:

Parâmetro IConfiguration

Ao criar uma biblioteca que exponha muitas opções aos consumidores, convém considerar a necessidade de um método de extensão de parâmetro IConfiguration. A instância esperada IConfiguration deve ser definida como escopo para uma seção nomeada da configuração usando a função 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;
    }
}

No código anterior, o AddMyLibraryService:

Os consumidores nesse padrão fornecem a instância IConfiguration em escopo da seção nomeada:

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();

A chamada para .AddMyLibraryService a ser feita no método IServiceCollection.

Como autor da biblioteca, a especificação de valores padrão cabe a você.

Observação

É possível associar a configuração a uma instância de opções. No entanto, há o risco de colisões de nome, o que causará erros. Além disso, ao associar manualmente dessa forma, você limita o consumo do padrão de opções para leitura uma vez. As alterações nas configurações não serão vinculadas novamente, pois esses consumidores não poderão usar a interface IOptionsMonitor .

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

Como alternativa, use o método de extensão BindConfiguration. Esse método de extensão associa a configuração à instância de opções e também registra uma fonte de token de alteração para a seção de configuração. Isso permite que os consumidores usem a interface IOptionsMonitor.

Parâmetro de caminho da seção de configuração

Os consumidores da biblioteca podem querer especificar o caminho da seção de configuração para vincular o tipo TOptions subjacente. Nesse cenário, você define um parâmetro string no método de extensão.

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;
    }
}

No código anterior, o AddMyLibraryService:

No próximo exemplo, o pacote NuGet Microsoft.Extensions.Options.DataAnnotations é usado para habilitar a validação de anotação de dados. A classe SupportOptions é definida da seguinte forma:

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 o seguinte arquivo JSON appsettings.json seja usado:

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

Parâmetro Action<TOptions>

Os consumidores de sua biblioteca podem estar interessados em fornecer uma expressão lambda que produz uma instância de sua classe de opções. Nesse cenário, você define um parâmetro Action<LibraryOptions> em seu método de extensão.

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;
    }
}

No código anterior, o AddMyLibraryService:

Os consumidores nesse padrão fornecem uma expressão lambda (ou um delegado que satisfaz o 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 instância de opções

Os consumidores de sua biblioteca podem preferir fornecer uma instância de opções embutidas. Nesse cenário, você expõe um método de extensão que usa uma instância do objeto de opções, 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;
    }
}

No código anterior, o AddMyLibraryService:

Os consumidores nesse padrão fornecem uma instância da classe LibraryOptions, definindo os valores de propriedade desejados embutidos:

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();

Pós-configuração

Depois que todos os valores de opção de configuração forem associados ou especificados, a funcionalidade pós-configuração estará disponível. Expondo o mesmo Action<TOptions> parâmetro detalhado anteriormente, você pode optar por chamar PostConfigure. A configuração de postagem é executada após todas as chamadas .Configure. Há alguns motivos pelos quais você deseja considerar o uso de PostConfigure:

  • Ordem de execução: você pode substituir todos os valores de configuração que foram definidos nas chamadas .Configure.
  • Validação: você pode validar se os valores padrão foram definidos depois que todas as outras configurações tiverem sido aplicadas.
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;
    }
}

No código anterior, o AddMyLibraryService:

Os consumidores nesse padrão fornecem uma expressão lambda (ou um delegado que satisfaz o parâmetro Action<LibraryOptions>), da mesma forma que fariam com o Action<TOptions> parâmetro em um cenário de configuração que seja pós-postagem:

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();

Confira também