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 segue dois princípios importantes de engenharia de software:

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

Associar configuração hierárquica

A maneira preferencial de ler os valores de configuração relacionados é usando o padrão de opções. O padrão de opções é possível por meio da interface IOptions<TOptions>, em que o parâmetro TOptions de tipo genérico é restrito a um class. O IOptions<TOptions> pode ser fornecido posteriormente por meio 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 destacados 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 classe TransientFaultHandlingOptions:

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

Ao usar o padrão de opções, uma classe de opções:

  • Precisa ser não abstrata e ter um construtor público sem parâmetros
  • Contém propriedades públicas de leitura e gravação a serem associadas (os campos não estão associados)

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

  • Chama ConfigurationBinder.Bind para associar a classe TransientFaultHandlingOptions à seção "TransientFaultHandlingOptions".
  • 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 seção "TransientFaultHandlingOptions" associada à instância TransientFaultHandlingOptions. Isso hidrata as propriedades de objetos C# com os valores correspondentes da configuração.

ConfigurationBinder.Get<T> associa 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 classe TransientFaultHandlingOptions:

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 TransientFaultHandlingOptions com seus valores de propriedade preenchidos usando a configuração subjacente.

Importante

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

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

No exemplo anterior, builder é uma instância de HostApplicationBuilder.

Dica

O parâmetro key é o nome da seção de configuração a ser pesquisada. Ele não precisa corresponder ao nome do tipo que o representa. Por exemplo, você pode ter uma seção nomeada "FaultHandling" e ela pode ser representada pela classe TransientFaultHandlingOptions. Nesse caso, você passará "FaultHandling" para a GetSection função em vez disso. O operador nameof é 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 a inicialização 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 conforme elas ocorrem e reagir adequadamente.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

O IOptionsFactory<TOptions> é responsável por criar novas instâncias de opções. Ele tem um único método Create. A implementação padrão usa todos os IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> registrados e executa todas as configurações primeiro, seguidas da pós-configuração. Ela faz distinção entre IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> e chama apenas a interface apropriada.

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

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

Benefícios das interfaces de opções

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

  • A avaliação da instância de configuração de T é adiada para o acesso de , em vez de IOptions<TOptions>.Value, quando ela é injetada. Isso é importante porque você pode consumir a opção T de vários lugares e escolher a semântica de vida sem alterar nada sobre T.
  • Ao registrar opções de tipo T, você não precisa registrar explicitamente o tipo T. 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 de DI com um tempo de vida específico.
  • Do ponto de vista da API, ela permite restrições no tipo T (nesse caso, T é restrita a um tipo de referência).

Usar IOptionsSnapshot para ler dados atualizados

Ao usar IOptionsSnapshot<TOptions>, as opções são calculadas uma vez por solicitação, quando acessadas e 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 permitem 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>.

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 associa a:

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

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

IOptionsMonitor

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 aplicativo iniciar a leitura.

Dica

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 interface IOptionsMonitor<TOptions> nesses ambientes, defina a variável de ambiente DOTNET_USE_POLLING_FILE_WATCHER como 1 ou true para sondar o sistema de arquivos para obter alterações. O intervalo no qual as alterações são sondadas é a cada quatro segundos e não é configurável.

Para obter mais informações sobre contêineres do Docker, consulte Colocar um aplicativo .NET no contêiner.

Compatibilidade de opções nomeadas usando IConfigureNamedOptions

Opções nomeadas:

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

Usando o seguinte arquivo appsettings.json:

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

Em vez de criar duas classes para associar 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 mostra as opções nomeadas:

public class sealed 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. As instâncias IConfigureOptions<TOptions> existentes são tratadas como sendo direcionadas à instância Options.DefaultName, que é string.Empty. IConfigureNamedOptions<TOptions> também implementa IConfigureOptions<TOptions>. A implementação padrão de IOptionsFactory<TOptions> tem lógica para usar cada um de forma adequada. A opção nomeada null é usada para direcionar todas as instâncias nomeadas, em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll usam essa convenção.

API OptionsBuilder

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

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

Usar os serviços de injeção de dependência para configurar as opções

Os serviços podem ser acessados de injeção de dependência ao configurar as opções de duas maneiras:

  • Passar um delegado de configuração para Configurar em OptionsBuilder<TOptions>. OptionsBuilder<TOptions> oferece sobrecargas de Configurar que permitem usar 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));
    
  • Criar um tipo que implementa IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registra o tipo como um serviço.

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

Validação de opções

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

Usando o seguinte arquivo appsettings.json:

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

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

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 propriedade ConfigurationSectionName contém o nome da seção de configuração à qual associar. Nesse cenário, o objeto de opções fornece o nome da sua seção de configuração.

Dica

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

O seguinte código:

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

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

O seguinte código 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 representante:

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ções 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 classe a seguir 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.

Observação

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

Usando o código anterior, a validação está habilitada ao configurar os 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>());

Pós-configuração de opções

Defina a pós-configuração com IPostConfigureOptions<TOptions>. A pós-configuração é executada depois que todas as configurações IConfigureOptions<TOptions> ocorrem e pode ser útil em cenários em que é necessário substituir a configuração:

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

O PostConfigure está disponível para pós-configurar opções nomeadas:

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

Confira também