Fevereiro de 2016

Volume 31 – Número 2

.NET Essencial – Configuração no .NET Core

Por Mark Michaelis

Antes da publicação na imprensa, a Microsoft anunciou alterações de nome relativas ao ASP.NET 5 e pilhas relacionadas. O ASP.NET 5 é agora o ASP.NET Core 1.0. O Entity Framework (EF) 7 é agora o Entity Framework (EF) Core 1.0. Os pacotes do EF7 e ASP.NET 5 e os namespaces serão alterados, mas, de qualquer forma, a nova nomenclatura não terá impacto nas lições deste artigo.

Mark MichaelisVocês que trabalham com o ASP.NET 5 perceberam com certeza o suporte da nova configuração incluído nessa plataforma e disponível na coleção Microsoft.Extensions.Configuration dos pacotes NuGet. A nova configuração permite criar uma lista de pares nome-valor, que pode ser agrupada em uma hierarquia de múltiplos níveis. Por exemplo, você pode ter uma configuração armazenada em SampleApp:Users:Inigo­Montoya:MaximizeMainWindow e outra armazenada em SampleApp:AllUsers:Default:MaximizeMainWindow. Os valores porventura armazenado são convertidos em uma cadeia de caracteres, com suporte de associação interno que permite que você desserialize as configurações em um objeto POCO personalizado. Os que já estiverem familiarizado com a nova configuração API, provavelmente a encontraram primeiro dentro do ASP.NET 5. No entanto, a API não é de forma alguma restrita ao ASP.NET. Na verdade, todas as listagens neste artigo foram criadas em um projeto de teste de unidade do Visual Studio 2015, com o Microsoft .NET Framework 4.5.1, mencionando os pacotes Microsoft.Extensions.Configuration do ASP.NET 5 RC1. (Acesse gitHub.com/IntelliTect/Articles para consultar o código-fonte.)

A API de configuração dá suporte aos provedores de configuração para objetos .NET na memória, a arquivos INI, arquivos JSON, arquivos XML, argumentos de linha de comando, variáveis de ambiente, um repositório de usuário criptografado e qualquer provedor personalizado que você criar. Se você desejar aproveitar os arquivos JSON para a sua configuração, basta adicionar o pacote NuGet Microsoft.Extensions.Configuration.Json. Depois, se quiser permitir que a linha de comando forneça informações de configuração, basta adicionar o pacote NuGet Microsoft.Extensions.Configuration.CommandLine, em adição ou substituição de outras referências de configuração. Se nenhum dos provedores de configuração interna forem satisfatórios, você pode criar sua própria configuração implementando as interfaces encontradas em Microsoft.Extensions.Configuration.Abstractions.

Recuperação das definições de configuração

Para você se familiarizar com recuperação das definições de configuração, veja a Figura 1.

Figura 1 Princípios básicos de configuração usando InMemoryConfigurationProvider e os métodos de extensão ConfigurationBinder

public class Program
{
  static public string DefaultConnectionString { get; } =
    @"Server=(localdb)\\mssqllocaldb;Database=SampleData-0B3B0919-C8B3-481C-9833-
    36C21776A565;Trusted_Connection=True;MultipleActiveResultSets=true";
  static IReadOnlyDictionary<string, string> DefaultConfigurationStrings{get;} =
    new Dictionary<string, string>()
    {
      ["Profile:UserName"] = Environment.UserName,
      [$"AppConfiguration:ConnectionString"] = DefaultConnectionString,
      [$"AppConfiguration:MainWindow:Height"] = "400",
      [$"AppConfiguration:MainWindow:Width"] = "600",
      [$"AppConfiguration:MainWindow:Top"] = "0",
      [$"AppConfiguration:MainWindow:Left"] = "0",
    };
  static public IConfiguration Configuration { get; set; }
  public static void Main(string[] args = null)
  {
    ConfigurationBuilder configurationBuilder =
      new ConfigurationBuilder();
      // Add defaultConfigurationStrings
      configurationBuilder.AddInMemoryCollection(
        DefaultConfigurationStrings);
      Configuration = configurationBuilder.Build();
      Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");
      ConsoleWindow consoleWindow =
        Configuration.Get<ConsoleWindow>("AppConfiguration:MainWindow");
      ConsoleWindow.SetConsoleWindow(consoleWindow);
  }
}

O acesso à configuração começa facilmente com uma instância do ConfigurationBuilder, uma classe disponível a partir do pacote NuGet Microsoft.Extensions.Configuration. Dada a instância do ConfigurationBuilder, você pode adicionar provedores diretamente usando os métodos de extensão do IConfigurationBuilder como o AddInMemoryCollection, como mostrado na Figura 1. Este método usa uma instância de Dictionary<string,string> dos pares nome-valor de configuração, que é usada para inicializar o provedor de configuração antes de adicioná-lo à instância ConfigurationBuilder. Quando o configuration builder estiver “configurado”, você invoca seu método Build para recuperar a configuração.

Como mencionado anteriormente, uma configuração é simplesmente uma lista hierárquica de pares nome-valor na qual os nós são separados por dois pontos. Portanto, para recuperar um valor específico, basta acessar o indexador de configuração com a chave do item correspondente:

Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");

No entanto, acessar um valor não se limita apenas a recuperar cadeias de caracteres. Você pode, por exemplo, recuperar valores por meio dos métodos de extensão Get<T> do ConfigurationBinder. Por exemplo, para recuperar o tamanho do buffer da tela da janela principal, você pode usar:

Configuration.Get<int>("AppConfiguration:MainWindow:ScreenBufferSize", 80);

Este suporte de associação requer uma referência para o pacote NuGet Microsoft.Exten­sions.Configuration.Binder.

Observe que existe um argumento opcional seguindo a chave, para a qual você pode especificar um valor padrão para retornar quando a chave não existe. (Sem o valor padrão, o retorno será atribuído como default(T) em vez de lançar uma exceção como você poderia esperar.)

Os valores de configuração não se limitam aos escalares. Você pode recuperar os objetos POCO ou mesmo gráficos de objeto completos. Para recuperar uma instância do ConsoleWindow cujos membros são convertidos para a seção de configuração AppConfiguration:MainWindow, a Figura 1 usa:

ConsoleWindow consoleWindow =
  Configuration.Get<ConsoleWindow>("AppConfiguration:MainWindow")

Em alternativa, você poderia definir um gráfico de configuração como o AppConfiguration, mostrado na Figura 2.

Figura 2 Uma configuração de amostra do gráfico de objeto

class AppConfiguration
{
  public ProfileConfiguration Profile { get; set; }
   public string ConnectionString { get; set; }
  public WindowConfiguration MainWindow { get; set; }
  public class WindowConfiguration
  {
    public int Height { get; set; }
    public int Width { get; set; }
    public int Left { get; set; }
    public int Top { get; set; }
  }
  public class ProfileConfiguration
  {
    public string UserName { get; set; }
  }
}
public static void Main()
{
  // ...
  AppConfiguration appConfiguration =
    Program.Configuration.Get<AppConfiguration>(
      nameof(AppConfiguration));
  // Requires referencing System.Diagnostics.TraceSource in Corefx
  System.Diagnostics.Trace.Assert(
    600 == appConfiguration.MainWindow.Width);
}

Com este tipo de gráfico de objeto, você poderia definir toda a sua configuração ou parte dela, com uma hierarquia de objeto com rigidez de tipos que você pode então usar para recuperar todas as suas configurações de uma vez.

Provedores de configuração múltiplos

O InMemoryConfigurationProvider é eficaz para armazenar valores padrão ou valores possivelmente calculados. No entanto, com apenas esse provedor, você fica com o inconveniente de recuperar a configuração e carregá-la em um Dictionary<string,string> antes de registrá-la com o ConfigurationBuilder. Felizmente, existem muito mais provedores de configuração internos, incluindo provedores baseados em três arquivos (XmlConfigurationProvider, IniConfigurationProvider e JsonConfigurationProvider); um provedor de variável de ambiente (EnvironmentVariableConfigurationProvider); e um provedor de argumento de linha de comando (CommandLineConfigurationProvider). Além disso, esses provedores podem ser misturados e combinados para se adaptarem à sua lógica de aplicativo. Imagine, por exemplo, que você especificasse as definições de configuração na seguinte prioridade ascendente:

  • InMemoryConfigurationProvider
  • JsonFileConfigurationProvider para Config.json
  • JsonFileConfigurationProvider para Config.Production.json
  • EnvironmentVariableConfigurationProvider
  • CommandLineConfigurationProvider

Em outras palavras, os valores de configuração padrão seriam armazenadas em código. Em seguida, o arquivo config.json seguido pelo Config.Production.json substituiria os valores especificados InMemory, nos quais provedores posteriores como os JSON têm prioridade sobre quaisquer valores sobrepostos. Em seguida, durante a implantação, você poderá ter valores de configuração personalizados armazenados nas variáveis de ambiente. Por exemplo, em vez de codificar Config.Production.json, você poderá recuperar a configuração de ambiente de uma variável de ambiente Windows e acessar o arquivo específico (talvez o Config.Test.Json) que a variável de ambiente identificar. (Desculpe a ambiguidade no termo configuração de ambiente referente a produção, teste, pré-produção ou desenvolvimento, em relação ao termo variáveis de ambiente do Windows, como %USERNAME% ou %USERDOMAIN%.) Por fim, você especifica (ou substitui) todas as configurações fornecidas anteriormente através da linha de comando – talvez como uma alteração única para, por exemplo, ligar registros.

Para especificar cada um dos provedores, adicione-os ao configuration builder (através do método de extensão de API fluente AddX), como mostrado na Figura 3.

Figura 3 Adicionando múltiplos provedores de configuração – o último especificado tem prioridade

public static void Main(string[] args = null)
{
  ConfigurationBuilder configurationBuilder =
    new ConfigurationBuilder();
  configurationBuilder
    .AddInMemoryCollection(DefaultConfigurationStrings)
    .AddJsonFile("Config.json",
      true) // Bool indicates file is optional
    // "EssentialDotNetConfiguartion" is an optional prefix for all
    // environment configuration keys, but once used,
    // only environment variables with that prefix will be found        
    .AddEnvironmentVariables("EssentialDotNetConfiguration")
    .AddCommandLine(
      args, GetSwitchMappings(DefaultConfigurationStrings));
  Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");
  AppConfiguration appConfiguration =
    Configuration.Get<AppConfiguration>(nameof(AppConfiguration));
}
static public Dictionary<string,string> GetSwitchMappings(
  IReadOnlyDictionary<string, string> configurationStrings)
{
  return configurationStrings.Select(item =>
    new KeyValuePair<string, string>(
      "-" + item.Key.Substring(item.Key.LastIndexOf(':')+1),
      item.Key))
      .ToDictionary(
        item => item.Key, item=>item.Value);
}

Para o JsonConfigurationProvider, você pode exigir que o arquivo exista ou pode torná-lo opcional, consequentemente, o parâmetro opcional adicional no AddJsonFile. Se nenhum parâmetro for fornecido, o arquivo é exigido e um sistema System.IO.FileNotFoundException será acionado se não for encontrado. Dada a natureza hierárquica do JSON, a configuração se ajusta muito bem na API de configuração (veja a Figura 4).

Figura 4 Dados de configuração do JSON para o JsonConfigurationProvider

{
  "AppConfiguration": {
    "MainWindow": {
      "Height": "400",
      "Width": "600",
      "Top": "0",
      "Left": "0"
    },
    "ConnectionString":
      "Server=(localdb)\\\\mssqllocaldb;Database=Database-0B3B0919-C8B3-481C-9833-
      36C21776A565;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

O CommandLineConfigurationProvider requer que você especifique os argumentos quando está registrado com o configuration builder. Os argumentos são especificados por uma matriz de cadeia de caracteres dos pares nome-valor do formato /<name>=<value>, no qual o sinal de igual é necessário. A barra à esquerda também é necessária, mas o segundo parâmetro da função AddCommandLine(string[] args, Dictionary<string,string> switchMappings) permite que você forneça aliases que devem ter o prefixo a - ou --. Por exemplo, um dicionário de valores permitirá que uma linha de comando de “program.exe -LogFile="c:\programdata\Application Data\Program.txt” carregue no elemento de configuração AppConfiguration:LogFile:

["-DBConnectionString"]="AppConfiguration:ConnectionString",
  ["-LogFile"]="AppConfiguration:LogFile"

Antes de terminar as noções básicas de configuração, aqui estão alguns pontos adicionais a serem observados:

  • O CommandLineConfigurationProvider tem várias características que não são intuitivas no IntelliSense e você deve estar ciente delas:
    • Os switchMappings do CommandLineConfigurationProvider permite somente um prefixo de opção de - ou –. Nem a barra (/) é permitida como parâmetro de opção. Isso evita que você forneça aliases para opções de barra através de mapeamentos de opção.
    • O CommandLineConfigurationProviders não permite argumentos de linha de comando baseados em opção, ou seja, argumentos que não incluem um valor atribuído. Especificar uma chave de “/Maximize,” por exemplo, não é permitido.
    • Enquanto você puder passar os args principais para uma nova instância CommandLineConfigurationProvider, você não pode passar Environment.GetCommandLineArgs sem antes remover o nome do processo. (Observe que o Environment.GetCommandLineArgs se comporta de forma diferente quando um depurador está anexado. Especificamente, os nomes executáveis com espaços são divididos em argumentos individuais quando não há depuradores anexados. Consulte itl.ty\GetCommandLineGotchas).
    • Uma exceção será usada quando você especifica um prefixo de opção - ou -- da linha de comando para o qual não existe mapeamento de opção correspondente.
  • Apesar de as configurações poderem ser atualizadas (Configuration["Profile:UserName"]="Inigo Montoya"), o valor atualizado não volta a persistir no repositório original. Por exemplo, quando você atribui um valor de configuração do provedor JSON, o arquivo JSON não será atualizado. Da mesma forma, uma variável de ambiente não seria atualizada quando seu item de configuração é atribuído.
  • O EnvironmentVariableConfigurationProvider permite como opção especificar um prefixo de chave. Nesses casos, ele carregará somente as variáveis de ambiente com o prefixo especificado. Dessa forma, você pode limitar automaticamente as entradas de configuração para as que estão dentro de uma “seção” de variável de ambiente ou, de forma mais ampla, para as que são relevantes para o seu aplicativo.
  • Há suporte para as variáveis de ambiente com um delimitador de dois pontos. Por exemplo, é permitido atribuir SET AppConfiguration:ConnectionString=Console na linha de comando.
  • As chaves de configuração (nomes) não diferenciam maiúsculas e minúsculas.
  • Cada provedor está localizado em seu próprio pacote NuGet, cujo nome corresponde ao provedor: Microsoft.Extensions.Configuration.CommandLine, Microsoft.Extensions.Configuration.EnvironmentVariables, Microsoft.Extensions.Configuration.Ini, Microsoft.Extensions.Configuration.Json e Microsoft.Extensions.Configuration.Xml.

Entender a estrutura orientada a objeto

Tanto a modularidade quanto a estrutura orientada a objeto da API de configuração estão bem pensadas, fornecendo classes detectáveis, modulares e facilmente extensíveis com o que trabalhar (veja a Figura 5).

Modelo de classe do provedor de configuração
Figura 5 Modelo de classe do provedor de configuração

Cada tipo de mecanismo de configuração possui uma classe de provedor de configuração correspondente que implementa o IConfigurationProvider. Na maioria das implementações de provedor interno, a implementação inicia derivando do ConfigurationBuilder em vez de usar implementações personalizadas para todos os métodos de interface. Pode ser surpreendente, mas não há referência direta a nenhum dos provedores na Figura 1. Isso acontece porque, em vez de instanciar manualmente cada provedor e registrá-lo com o método Add da classe do ConfigurationBuilder, cada pacote NuGet do provedor inclui uma classe de extensão estática com os métodos de extensão IConfigurationBuilder. (O nome da classe de extensão é geralmente identificado pelo sufixo ConfigurationExtensions.) Com as classes de extensão, você pode começar acessando os dados de configuração diretamente do ConfigurationBuilder (que implementa o IConfigurationBuilder) e chamar diretamente o método de extensão associado ao seu provedor. Por exemplo, a classe JasonConfigurationExtensions adiciona os métodos de extensão AddJsonFile ao IConfigurationBuilder de forma que você possa adicionar a configuração JSON com uma chamada para Configuration­Builder.AddJsonFile(fileName, optional).Build();.

Em geral, uma vez que você tenha a configuração, terá tudo o que precisa para começar a recuperar os valores.

O IConfiguration inclui um indexador de cadeia de caracteres, permitindo que você recupere qualquer valor de configuração particular usando a chave para acessar o elemento que você está procurando. Você pode recuperar todo um conjunto de configurações (chamado seção) com os métodos GetSection ou GetChildren (dependendo se você quer detalhar um nível adicional na hierarquia). Observe que as seções do elemento de configuração permitem que você recupere o seguinte:

  • chave: o último elemento do nome.
  • caminho: o nome completo apontando para a raiz do local atual.
  • valor: o valor de configuração armazenado na definição de configuração.
  • valor como objeto: através do ConfigurationBinder, é possível recuperar um objeto POCO que corresponde à seção de configuração que você está acessando (e potencialmente seus filhos). É assim que o Configuration.Get<AppConfiguration>(nameof(App­Configuration)) trabalha na Figura 3, por exemplo.
  • O IConfigurationRoot inclui uma função Reload que permite que você recarregue os valores para atualizar a configuração. O ConfigurationRoot (que implementa o IConfigurationRoot) inclui um método GetReloadToken que permite que você registre notificações de quando ocorre uma recarga (e o valor pode mudar).

Configurações criptografadas

Às vezes, você vai querer recuperar ajustes que estão criptografados em vez de armazenados em texto aberto. Isso é importante, por exemplo, quando você está armazenando chaves de aplicativo ou tokens OAuth, ou credenciais de armazenamento para cadeia de conexão de banco de dados. Felizmente, o sistema Microsoft.Extensions.Configuration tem um suporte interno para ler os valores criptografados. Para acessar o repositório seguro, você precisa adicionar uma referência ao pacote NuGet do Microsoft.Extensions.Configuration.User­Secrets. Depois de ser adicionado, você terá um novo método de extensão IConfigurationBuilder.AddUserSecrets que usa um argumento de cadeia de caracteres do item de configuração chamado userSecretsId (armazenado em seu arquivo project.json). Como você já previa, uma vez que a configuração UserSecrets é adicionada ao seu configuration builder, você pode começar a recuperar os valores criptografados, que só os usuários que possuem configurações associadas podem acessar.

Obviamente, a recuperação de configurações não faz muito sentido se você também não puder configurá-las. Para fazer isso, use a ferramenta user-secret.cmd da seguinte forma:

user-secret set <secretName> <value> [--project <projectPath>]

A opção --project permite que você associe a configuração com o valor userSecretsId armazenado em seu arquivo project.json (criado por padrão pelo novo assistente de projeto ASP.NET 5). Se você não tem uma ferramenta secreta de usuário, precisará adicioná-la através do prompt de comando do desenvolvedor usando o utilitário DNX (atualmente dnu.exe).

Para mais informações sobre a opção de configuração secreta do usuário, consulte o artigo “Safe Storage of Application Secrets,” por Rick Anderson e David Roth em bit.ly/1mmnG0L.

Conclusão

Quem já usa o .NET há algum tempo ficou provavelmente decepcionado com o suporte interno para configuração através do System.Configuration. Isso deve ser mais verdadeiro ainda se você usava o ASP.NET clássico, no qual a configuração estava limitada aos arquivos Web.Config ou App.config e, depois, somente acessando o nó AppSettings nela. Felizmente, a nova API de código-fonte aberto Microsoft.Extensions.Configuration vai muito além do que estava originalmente disponível, por adicionar uma infinidade de novos provedores de configuração, junto com um sistema facilmente extensível no qual você pode conectar qualquer provedor personalizado que quiser. Para aqueles que ainda estão vivendo (ou estão presos?) em um mundo pré ASP.NET 5, as APIs antigas System.Configuration APIs ainda funcionam, mas você pode começar lentamente a migrar (mesmo lado a lado) para a nova API apenas mencionando os novos pacotes. Além disso, os pacotes NuGet podem ser usados com projetos de cliente Windows, como o console e os aplicativos do Windows Presentation Foundation. Portanto, da próxima vez que você precisar acessar dados de configuração, não haverá desculpa para não aproveitar a API Microsoft.Extensions.Configuration.


Mark Michaelis * é fundador da IntelliTect, onde atua como arquiteto técnico principal e instrutor. Há quase 20 anos trabalha como Microsoft MVP, e é Diretor Regional da Microsoft desde 2007. Michaelis atua em diversas equipes de análise de design de software da Microsoft, incluindo C#, Microsoft Azure, SharePoint e Visual Studio ALM. Ele dá palestras em conferências de desenvolvedores e escreveu diversos livros, incluindo o mais recente, "Essential C# 6.0 (5th Edition)" (itl.tc/­EssentialCSharp). Você pode entrar em contato com ele pelo Facebook, em facebook.com/Mark.Michaelis, pelo seu blog IntelliTect.com/Mark, no Twitter: @markmichaelis ou pelo email mark@IntelliTect.com.*

Agradecemos aos seguintes especialistas técnicos da IntelliTect pela revisão deste artigo: Grant Erickson, Derek Howard, Phil Spokas e Michael Stokesbary