Orientação de padrão de opções para autores de bibliotecas .NET
Com a ajuda da injeção de dependência, registrar seus serviços e suas configurações correspondentes pode fazer uso do 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 onde TOptions
está 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 elimina a carga de analisar manualmente valores de cadeia de caracteres. Há muitos provedores de configuração para os consumidores da sua biblioteca usarem. Com esses provedores, os consumidores podem configurar sua biblioteca de várias maneiras.
Como autor de uma biblioteca .NET, você aprenderá orientações gerais sobre como expor corretamente o padrão de opções aos consumidores da sua biblioteca. Há várias maneiras de conseguir a mesma coisa, e várias considerações a fazer.
Convenções de nomenclatura
Por convenção, os métodos de extensão responsáveis pelo registro de serviços são nomeados Add{Service}
, onde {Service}
é um nome significativo e descritivo. Add{Service}
métodos de extensão são comuns no ASP.NET Core e no .NET.
✔️ CONSIDERE nomes que desambiguem seu serviço de outras ofertas.
❌ NÃO use nomes que já fazem parte do ecossistema .NET de pacotes oficiais da Microsoft.
✔️ CONSIDERE nomear classes estáticas que expõem métodos de extensão como {Type}Extensions
, onde {Type}
é o tipo que você está estendendo.
Diretrizes de namespace
Os pacotes da Microsoft usam o Microsoft.Extensions.DependencyInjection
namespace 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 para pacotes não oficiais da Microsoft.Extensions.DependencyInjection
Microsoft.
Sem parâmetros
Se o serviço puder funcionar com pouca ou nenhuma configuraçã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
:
- Estende uma instância de IServiceCollection
- Chamadas OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) com o parâmetro type de
LibraryOptions
- Encadeia uma chamada para Configure, que especifica os valores de opção padrão
IConfiguration
parâmetro
Ao criar uma biblioteca que expõe muitas opções aos consumidores, convém considerar a necessidade de um IConfiguration
método de extensão de parâmetro. A instância esperada IConfiguration
deve ter o escopo definido para uma seção nomeada da configuração usando a IConfiguration.GetSection função.
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;
}
}
Gorjeta
O Configure<TOptions>(IServiceCollection, IConfiguration) método faz parte do Microsoft.Extensions.Options.ConfigurationExtensions
pacote NuGet.
No código anterior, o AddMyLibraryService
:
- Estende uma instância de IServiceCollection
- Define um IConfiguration parâmetro
namedConfigurationSection
- Chamadas Configure<TOptions>(IServiceCollection, IConfiguration) que passam o parâmetro de tipo genérico de e a
namedConfigurationSection
instância a ser configuradaLibraryOptions
Os consumidores nesse padrão fornecem a instância com IConfiguration
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
é feita no IServiceCollection tipo.
Como autor da biblioteca, a especificação de valores padrão depende de você.
Nota
É possível vincular a configuração a uma instância de opções. No entanto, há um risco de colisões de nomes - o que causará erros. Além disso, ao vincular manualmente dessa forma, você limita o consumo do padrão de opções para leitura única. As alterações nas configurações não serão redirecionadas, pois esses consumidores não poderão usar a interface IOptionsMonitor .
services.AddOptions<LibraryOptions>()
.Configure<IConfiguration>(
(options, configuration) =>
configuration.GetSection("LibraryOptions").Bind(options));
Em vez disso, você deve usar o BindConfiguration método de extensão. Esse método de extensão vincula 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 sua biblioteca podem querer especificar o caminho da seção de configuração para vincular seu tipo subjacente TOptions
. Nesse cenário, você define um string
parâmetro 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,
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
:
- Estende uma instância de IServiceCollection
- Define um
string
parâmetroconfigSectionPath
- Convocatórias:
- AddOptions com o parâmetro de tipo genérico de
SupportOptions
- BindConfigurationcom o parâmetro dado
configSectionPath
- ValidateDataAnnotations Para habilitar a validação de anotação de dados
- ValidateOnStart Para impor a validação no início em vez de em tempo de execução
- AddOptions com o parâmetro de tipo genérico de
No próximo exemplo, o pacote NuGet Microsoft.Extensions.Options.DataAnnotations é usado para habilitar a validação de anotação de dados. A SupportOptions
classe é 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 é usado:
{
"Support": {
"Url": "https://support.example.com",
"Email": "help@support.example.com",
"PhoneNumber": "+1(888)-SUPPORT"
}
}
Action<TOptions>
parâmetro
Os consumidores de sua biblioteca podem estar interessados em fornecer uma expressão lambda que produza uma instância de sua classe de opções. Nesse cenário, você define um Action<LibraryOptions>
parâmetro 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
:
- Estende uma instância de IServiceCollection
- Define um Action<T> parâmetro
configureOptions
ondeT
éLibraryOptions
- Apelos Configure tendo em conta a
configureOptions
ação
Os consumidores nesse padrão fornecem uma expressão lambda (ou um delegado que satisfaz o Action<LibraryOptions>
parâmetro):
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 da 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 seu objeto options, 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
:
- Estende uma instância de IServiceCollection
- Chamadas OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) com o parâmetro type de
LibraryOptions
- Encadeia uma chamada para Configure, que especifica valores de opção padrão que podem ser substituídos a partir de determinada
userOptions
instância
Os consumidores nesse padrão fornecem uma instância da classe, definindo os valores de LibraryOptions
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 vinculados 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 pós-configuração é executada após todas as .Configure
chamadas. Há algumas razões pelas quais você gostaria de considerar o uso PostConfigure
:
- Ordem de execução: Você pode substituir quaisquer valores de configuração que foram definidos nas
.Configure
chamadas. - Validação: Você pode validar que os valores padrão foram definidos depois que todas as outras configurações foram 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
:
- Estende uma instância de IServiceCollection
- Define um Action<T> parâmetro
configureOptions
ondeT
éLibraryOptions
- Apelos PostConfigure tendo em conta a
configureOptions
ação
Os consumidores nesse padrão fornecem uma expressão lambda (ou um delegado que satisfaz o Action<LibraryOptions>
parâmetro), assim como fariam com o Action<TOptions>
parâmetro em um cenário de configuração não-post:
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();