Ler em inglês

Partilhar via


Padrão de opções no .NET

O padrão de opções usa classes para fornecer acesso fortemente tipado a grupos de configurações relacionadas. Quando as definições de configuração são isoladas por cenário em classes separadas, o aplicativo adere a dois princípios importantes de engenharia de software:

As opções também fornecem um mecanismo para validar dados de configuração. Para obter mais informações, consulte a seção Validação de opções.

Vincular configuração hierárquica

A maneira preferida de ler os valores de configuração relacionados é usando o padrão de opções. O padrão de opções é possível através da IOptions<TOptions> interface, onde o parâmetro TOptions de tipo genérico é restrito a um class. O IOptions<TOptions> pode mais tarde ser fornecido através de injeção de dependência. Para obter mais informações, consulte Injeção de dependência no .NET.

Por exemplo, para ler os valores de configuração realçados de um arquivo appsettings.json :

{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

Crie a seguinte TransientFaultHandlingOptions classe:

public sealed class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}

Ao usar o padrão options, uma classe options:

  • Deve ser não-abstrato com um construtor público sem parâmetros
  • Contêm propriedades públicas de leitura-gravação para vincular (os campos não são vinculados)

O código a seguir faz parte do arquivo C# Program.cs e:

  • Chama ConfigurationBinder.Bind para vincular a TransientFaultHandlingOptions classe à "TransientFaultHandlingOptions" seção.
  • Exibe os dados de configuração.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();

IHostEnvironment env = builder.Environment;

builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);

TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
    .Bind(options);

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

// <Output>
// Sample output:

No código anterior, o arquivo de configuração JSON tem sua "TransientFaultHandlingOptions" seção vinculada à TransientFaultHandlingOptions instância. Isso hidrata as propriedades dos objetos C# com os valores correspondentes da configuração.

ConfigurationBinder.Get<T> Vincula e retorna o tipo especificado. ConfigurationBinder.Get<T> pode ser mais conveniente do que usar ConfigurationBinder.Bind. O código a seguir mostra como usar ConfigurationBinder.Get<T> com a TransientFaultHandlingOptions classe:

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

No código anterior, o ConfigurationBinder.Get<T> é usado para adquirir uma instância do objeto com seus valores de propriedade preenchidos TransientFaultHandlingOptions a partir da configuração subjacente.

Importante

A ConfigurationBinder classe expõe várias APIs, como .Bind(object instance) e .Get<T>() que não estão restritas a class. Ao usar qualquer uma das interfaces Opções, você deve aderir às restrições de classe de opções acima mencionadas.

Uma abordagem alternativa ao usar o padrão de opções é vincular a "TransientFaultHandlingOptions" seção e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, TransientFaultHandlingOptions é adicionado ao contêiner de serviço com Configure e vinculado à configuração:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

O builder exemplo anterior é uma instância de HostApplicationBuilder.

Gorjeta

O key parâmetro é o nome da seção de configuração a ser pesquisada. Não tem de corresponder ao nome do tipo que o representa. Por exemplo, você pode ter uma seção nomeada "FaultHandling" e ela pode ser representada pela TransientFaultHandlingOptions classe. Neste caso, você passaria "FaultHandling" para a GetSection função. O nameof operador é usado como uma conveniência quando a seção nomeada corresponde ao tipo ao qual corresponde.

Usando o código anterior, o código a seguir lê as opções de posição:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo não são lidas. Para ler as alterações após o início do aplicativo, use IOptionsSnapshot ou IOptionsMonitor para monitorar as alterações à medida que elas ocorrem e reagir de acordo.

Opções de interfaces

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> é responsável pela criação de novas instâncias de opções. Tem um único Create método. A implementação padrão leva todos os registrados IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> executa todas as configurações primeiro, seguido pela pós-configuração. Ele distingue entre IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> só chama a interface apropriada.

IOptionsMonitorCache<TOptions> é usado por para armazenar instâncias em IOptionsMonitor<TOptions> cache TOptions . O IOptionsMonitorCache<TOptions> invalida instâncias de opções no monitor para que o valor seja recalculado (TryRemove). Os valores podem ser introduzidos manualmente com TryAdd. O Clear método é usado quando todas as instâncias nomeadas devem ser recriadas sob demanda.

IOptionsChangeTokenSource<TOptions> é usado para buscar o que controla IChangeToken as alterações na instância subjacente TOptions . Para obter mais informações sobre primitivos de token de alteração, consulte Alterar notificações.

Vantagens das interfaces de opções

O uso de um tipo de wrapper genérico oferece a capacidade de dissociar o tempo de vida da opção do contêiner de injeção de dependência (DI). A IOptions<TOptions>.Value interface fornece uma camada de abstração, incluindo restrições genéricas, no seu tipo de opções. Isso proporciona os seguintes benefícios:

  • A avaliação da instância de configuração é adiada T para o acesso de , em vez de IOptions<TOptions>.Valuequando ela é injetada. Isso é importante porque você pode consumir a T opção de vários lugares e escolher a semântica vitalícia sem alterar nada sobre T.
  • Ao registrar opções do tipo T, você não precisa registrar explicitamente o T tipo. Essa é uma conveniência quando você está criando uma biblioteca com padrões simples e não quer forçar o chamador a registrar opções no contêiner DI com um tempo de vida específico.
  • Do ponto de vista da API, ele permite restrições sobre o tipo T (neste caso, T é restrito a um tipo de referência).

Use IOptionsSnapshot para ler dados atualizados

Quando você usa IOptionsSnapshot<TOptions>o , as opções são calculadas uma vez por solicitação quando acessadas e são armazenadas em cache durante o tempo de vida da solicitação. As alterações na configuração são lidas depois que o aplicativo é iniciado ao usar provedores de configuração que suportam a leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço singleton que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências singleton.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o IOptionsSnapshot<T> objeto é construído. Os instantâneos de opções são projetados para uso com dependências transitórias e com escopo.

O código a seguir usa IOptionsSnapshot<TOptions>o .

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

O código a seguir registra uma instância de configuração que TransientFaultHandlingOptions se liga contra:

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

No código anterior, o Configure<TOptions> método é usado para registrar uma instância de configuração que TOptions será vinculada e atualiza as opções quando a configuração é alterada.

IOptionsMonitor

O IOptionsMonitor tipo suporta notificações de alteração e permite cenários em que seu aplicativo pode precisar responder às alterações de origem de configuração dinamicamente. Isso é útil quando você precisa reagir a alterações nos dados de configuração depois que o aplicativo é iniciado. As notificações de alteração só são suportadas para provedores de configuração baseados em sistema de arquivos, como os seguintes:

Para usar o monitor de opções, os objetos de opções são configurados da mesma maneira em uma seção de configuração.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

O exemplo a seguir usa IOptionsMonitor<TOptions>:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
    public void DisplayValues()
    {
        TransientFaultHandlingOptions options = monitor.CurrentValue;

        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
    }
}

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

Gorjeta

Alguns sistemas de arquivos, como contêineres do Docker e compartilhamentos de rede, podem não enviar notificações de alteração de forma confiável. Ao usar a IOptionsMonitor<TOptions> interface nesses ambientes, defina a DOTNET_USE_POLLING_FILE_WATCHER variável de ambiente para 1 ou true sonde o sistema de arquivos em busca de alterações. O intervalo em que as alterações são pesquisadas é a cada quatro segundos e não é configurável.

Para obter mais informações sobre contêineres do Docker, consulte Containerize a .NET app.

Suporte a opções nomeadas usando IConfigureNamedOptions

Opções nomeadas:

  • São úteis quando várias seções de configuração se ligam às mesmas propriedades.
  • Diferenciam maiúsculas de minúsculas.

Considere o seguinte arquivo appsettings.json :

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

Em vez de criar duas classes para vincular Features:Personalize e Features:WeatherStation, a seguinte classe é usada para cada seção:

public class Features
{
    public const string Personalize = nameof(Personalize);
    public const string WeatherStation = nameof(WeatherStation);

    public bool Enabled { get; set; }
    public string ApiKey { get; set; }
}

O código a seguir configura as opções nomeadas:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

O código a seguir exibe as opções nomeadas:

public sealed class Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

Todas as opções são instâncias nomeadas. IConfigureOptions<TOptions> as instâncias são tratadas como direcionadas à Options.DefaultName instância, que é string.Empty. IConfigureNamedOptions<TOptions> também implementa IConfigureOptions<TOptions>. A implementação padrão do IOptionsFactory<TOptions> tem lógica para usar cada um adequadamente. A null opção nomeada é usada para direcionar todas as instâncias nomeadas em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll utilizar esta convenção.

OptionsBuilder API

OptionsBuilder<TOptions> é usado para configurar TOptions instâncias. OptionsBuilder simplifica a criação de opções nomeadas, pois é apenas um único parâmetro para a chamada inicial AddOptions<TOptions>(string optionsName) , em vez de aparecer em todas as chamadas subsequentes. A validação de opções e as ConfigureOptions sobrecargas que aceitam dependências de serviço só estão disponíveis via OptionsBuilder.

OptionsBuilderé usado na seção Validação de opções.

Usar serviços de DI para configurar opções

Ao configurar opções, você pode usar a injeção de dependência para acessar serviços registrados e usá-los para configurar opções. Isso é útil quando você precisa acessar serviços para configurar opções. Os serviços podem ser acessados a partir do DI enquanto configuram as opções de duas maneiras:

  • Passe um delegado de configuração para Configurar no OptionsBuilder<TOptions>. OptionsBuilder<TOptions> fornece sobrecargas de Configure que permitem o uso de até cinco serviços para configurar opções:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Crie um tipo que implemente IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registre o tipo como um serviço.

É recomendável passar um delegado de configuração para o Configure, já que a criação de um serviço é mais complexa. Criar um tipo é equivalente ao que a estrutura faz ao chamar Configure. Chamar Configure registra um genérico IConfigureNamedOptions<TOptions>transitório , que tem um construtor que aceita os tipos de serviço genéricos especificados.

Validação de opções

A validação de opções permite que os valores das opções sejam validados.

Considere o seguinte arquivo appsettings.json :

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

A classe a seguir se liga à "MyCustomSettingsSection" seção de DataAnnotations configuração e aplica algumas regras:

using System.ComponentModel.DataAnnotations;

namespace ConsoleJson.Example;

public sealed class SettingsOptions
{
    public const string ConfigurationSectionName = "MyCustomSettingsSection";

    [Required]
    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public required string SiteTitle { get; set; }

    [Required]
    [Range(0, 1_000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public required int Scale { get; set; }

    [Required]
    public required int VerbosityLevel { get; set; }
}

Na classe anterior SettingsOptions , a ConfigurationSectionName propriedade contém o nome da seção de configuração à qual se vincular. Nesse cenário, o objeto options fornece o nome de sua seção de configuração.

Gorjeta

O nome da seção de configuração é independente do objeto de configuração ao qual está vinculado. Em outras palavras, uma seção de configuração chamada "FooBarOptions" pode ser vinculada a um objeto de opções chamado ZedOptions. Embora possa ser comum nomeá-los da mesma forma, não é necessário e pode realmente causar conflitos de nomes.

O seguinte código:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

O ValidateDataAnnotations método de extensão é definido no pacote NuGet Microsoft.Extensions.Options.DataAnnotations.

O código a seguir exibe os valores de configuração ou relata erros de validação:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ValidationService
{
    private readonly ILogger<ValidationService> _logger;
    private readonly IOptions<SettingsOptions> _config;

    public ValidationService(
        ILogger<ValidationService> logger,
        IOptions<SettingsOptions> config)
    {
        _config = config;
        _logger = logger;

        try
        {
            SettingsOptions options = _config.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (string failure in ex.Failures)
            {
                _logger.LogError("Validation error: {FailureMessage}", failure);
            }
        }
    }
}

O código a seguir aplica uma regra de validação mais complexa usando um delegado:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

A validação ocorre em tempo de execução, mas você pode configurá-la para ocorrer na inicialização encadeando uma chamada para ValidateOnStart:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.")
    .ValidateOnStart();

A partir do .NET 8, você pode usar uma API alternativa, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), que permite a validação no início para um tipo de opção específico:

builder.Services
    .AddOptionsWithValidateOnStart<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

IValidateOptions para validação complexa

A seguinte classe implementa IValidateOptions<TOptions>:

using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

sealed partial class ValidateSettingsOptions(
    IConfiguration config)
    : IValidateOptions<SettingsOptions>
{
    public SettingsOptions? Settings { get; private set; } =
        config.GetSection(SettingsOptions.ConfigurationSectionName)
              .Get<SettingsOptions>();

    public ValidateOptionsResult Validate(string? name, SettingsOptions options)
    {
        StringBuilder? failure = null;
    
        if (!ValidationRegex().IsMatch(options.SiteTitle))
        {
            (failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
        }

        if (options.Scale is < 0 or > 1_000)
        {
            (failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
        }

        if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
        {
            (failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
        }

        return failure is not null
            ? ValidateOptionsResult.Fail(failure.ToString())
            : ValidateOptionsResult.Success;
    }

    [GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
    private static partial Regex ValidationRegex();
}

IValidateOptions Permite mover o código de validação para uma classe.

Nota

Este código de exemplo depende do pacote NuGet Microsoft.Extensions.Configuration.Json .

Usando o código anterior, a validação é habilitada ao configurar serviços com o seguinte código:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

Opções pós-configuração

Defina a pós-configuração com IPostConfigureOptions<TOptions>o . A pós-configuração é executada depois que toda a IConfigureOptions<TOptions> configuração ocorre e pode ser útil em cenários em que você precisa substituir a configuração:

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

PostConfigure está disponível para opções nomeadas pós-configuração:

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Use PostConfigureAll para pós-configurar todas as instâncias de configuração:

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Consulte também