Vamos adiar a conversa sobre DDD e reflexão por um tempo. Proponho falar sobre o simples, sobre a organização das configurações do aplicativo.
Depois que meus colegas e eu decidimos mudar para o .NET Core, surgiu a questão de como organizar arquivos de configuração, como realizar transformações etc. no novo ambiente. O código a seguir é encontrado em muitos exemplos e muitos o utilizaram com êxito.
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Mas vamos ver como a configuração funciona, e em quais casos usar essa abordagem e em que confiar nos desenvolvedores do .NET Core. Eu peço gato.
Como era antes
Como qualquer história, este artigo tem um começo. Um dos primeiros problemas após a mudança para o ASP.NET Core foi a transformação dos arquivos de configuração.
Lembre-se de como era antes com o web.config
A configuração consistiu em vários arquivos. O arquivo principal era web.config e as transformações ( web.Development.config , etc.) já foram aplicadas a ele, dependendo da configuração do assembly. Ao mesmo tempo, os atributos xml foram usados ativamente para pesquisar e transformar a seção do documento xml .
Mas, como sabemos no ASP.NET Core, o arquivo web.config foi substituído por appsettings.json e não existe mais o mecanismo de transformação usual.
O que o google nos diz?O resultado da pesquisa para "Transformando em ASP.NET Core" no google resultou no seguinte código:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
No construtor da classe Startup , criamos um objeto de configuração usando o ConfigurationBuilder . Nesse caso, indicamos explicitamente quais fontes de configuração queremos usar.
E tal:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Dependendo da variável de ambiente, uma ou outra fonte de configuração é selecionada.
Essas respostas são frequentemente encontradas no SO e em outros recursos menos populares. Mas o sentimento não se foi. que estamos errando. E se eu quiser usar variáveis de ambiente ou argumentos de linha de comando na configuração? Por que preciso escrever esse código em todos os projetos?
Em busca da verdade, tive que me aprofundar na documentação e no código fonte. E quero compartilhar o conhecimento adquirido neste artigo.
Vamos ver como a configuração funciona no .NET Core.
Configuração
A configuração no .NET Core é representada por um objeto de interface IConfiguration .
public interface IConfiguration { string this[string key] { get; set; } IConfigurationSection GetSection(string key); IEnumerable<IConfigurationSection> GetChildren(); IChangeToken GetReloadToken(); }
- indexador [string key] , que permite obter o valor do parâmetro de configuração por chave
- GetSection (string string) retorna a seção de configuração que corresponde à chave da chave
- GetChildren () retorna um conjunto de subseções da seção de configuração atual
- GetReloadToken () retorna uma instância do IChangeToken que pode ser usada para receber notificações quando a configuração for alterada
Uma configuração é uma coleção de pares de valores-chave. Ao ler de uma fonte de configuração (arquivo, variáveis de ambiente), os dados hierárquicos são reduzidos a uma estrutura plana. Por exemplo, json é um objeto do formulário
{ "Settings": { "Key": "I am options" } }
será reduzido para uma visualização plana:
Settings:Key = I am options
Aqui, a chave é Configurações: Chave , e o valor é Eu sou opções .
Os provedores de configuração são usados para preencher a configuração.
Provedores de configuração
Um objeto de interface é responsável pela leitura de dados da fonte de configuração.
IConfigurationProvider :
public interface IConfigurationProvider { bool TryGet(string key, out string value); void Set(string key, string value); IChangeToken GetReloadToken(); void Load(); IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); }
- TryGet (chave da string, valor da string out) permite obter o valor do parâmetro de configuração pela chave
- Set (chave da string, valor da string) é usado para definir o valor do parâmetro de configuração
- GetReloadToken () retorna uma instância do IChangeToken que pode ser usada para receber notificações quando uma fonte de configuração é alterada
- Método Load () responsável pela leitura da fonte de configuração
- GetChildKeys (IEnumerable <string> previousKeys, string parentPath) permite obter uma lista de todas as chaves que esse provedor de configuração fornece
Os seguintes provedores estão disponíveis na caixa:
- Json
- Ini
- Xml
- Variáveis de ambiente
- Memória
- Azure
- Provedor de configuração personalizada
As convenções a seguir para usar provedores de configuração são aceitas.
- As fontes de configuração são lidas na ordem em que foram especificadas.
- Se as mesmas chaves estiverem presentes em diferentes fontes de configuração (a comparação não diferencia maiúsculas de minúsculas), o valor adicionado por último é usado.
Se criarmos uma instância de um servidor da Web usando CreateDefaultBuilder , os seguintes provedores de configuração serão conectados por padrão:

- ChainedConfigurationProvider através deste provedor, você pode obter valores e chaves de configuração que foram adicionados por outros provedores de configuração
- JsonConfigurationProvider usa arquivos json como a fonte de configuração. Como você pode ver, três provedores deste tipo são adicionados à lista de provedores. O primeiro usa appsettings.json como fonte, o segundo usa appsettings. {Environment} .json . O terceiro lê dados de secrets.json . Se você criar o aplicativo na configuração do Release , o terceiro provedor não será conectado, porque não é recomendável usar segredos no ambiente de produção
- EnvironmentVariablesConfigurationProvider recupera parâmetros de configuração de variáveis de ambiente
- CommandLineConfigurationProvider permite adicionar argumentos de linha de comando à configuração
Como a configuração é armazenada como um dicionário, é necessário garantir a exclusividade das chaves. Por padrão, isso funciona assim.
Se o provedor CommandLineConfigurationProvider tiver um elemento com a chave key e o provedor JsonConfigurationProvider tiver um elemento com a chave key, o elemento JsonConfigurationProvider será substituído pelo elemento CommandLineConfigurationProvider à medida que for registrado por último e tiver uma prioridade mais alta.
Lembre-se de um exemplo desde o início do artigo public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Não precisamos criar IConfiguration para transformar os arquivos de configuração, pois isso é ativado por padrão. Essa abordagem é necessária quando queremos limitar o número de fontes de configuração.
Provedor de configuração personalizada
Para escrever seu provedor de configuração, você precisa implementar as interfaces IConfigurationProvider e IConfigurationSource . IConfigurationSource é uma nova interface que ainda não consideramos neste artigo.
public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }
A interface consiste em um único método Build que usa o IConfigurationBuilder como parâmetro e retorna uma nova instância do IConfigurationProvider .
Para implementar nossos provedores de configuração, as classes abstratas ConfigurationProvider e FileConfigurationProvider estão disponíveis para nós. Nessas classes, a lógica dos métodos TryGet , Set , GetReloadToken , GetChildKeys já está implementada e resta implementar apenas o método Load .
Vejamos um exemplo. É necessário implementar a leitura da configuração do arquivo yaml , e também é necessário que possamos alterar a configuração sem reiniciar nosso aplicativo.
Crie a classe YamlConfigurationProvider e torne-a herdeira do FileConfigurationProvider .
public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); } }
No trecho de código acima, você pode observar alguns recursos da classe FileConfigurationProvider . O construtor aceita uma instância de FileConfigurationSource , que contém o IFileProvider . IFileProvider é usado para ler um arquivo e assinar um evento de alteração de arquivo. Você também pode observar que o método Load aceita um Stream no qual o arquivo de configuração está aberto para leitura. Este é um método da classe FileConfigurationProvider e não está na interface IConfigurationProvider .
Adicione uma implementação simples que nos permita ler o arquivo yaml . Para ler o arquivo, usarei o pacote YamlDotNet .
Implementação de YamlConfigurationProvider public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { if (stream.CanSeek) { stream.Seek(0L, SeekOrigin.Begin); using (StreamReader streamReader = new StreamReader(stream)) { var fileContent = streamReader.ReadToEnd(); var yamlObject = new DeserializerBuilder() .Build() .Deserialize(new StringReader(fileContent)) as IDictionary<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> pair) { var key = String.IsNullOrEmpty(prefix) ? pair.Key.ToString() : $"{prefix}:{pair.Key}"; switch (pair.Value) { case string value: Data.Add(key, value); break; case IDictionary<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } }
Para criar uma instância do nosso provedor de configuração, você deve implementar o FileConfigurationSource .
Implementação YamlConfigurationSource public class YamlConfigurationSource : FileConfigurationSource { public YamlConfigurationSource(string fileName) { Path = fileName; ReloadOnChange = true; } public override IConfigurationProvider Build(IConfigurationBuilder builder) { this.EnsureDefaults(builder); return new YamlConfigurationProvider(this); } }
É importante observar aqui que, para inicializar as propriedades da classe base, você deve chamar o método this.EnsureDefaults (builder) .
Para registrar um provedor de configuração personalizado no aplicativo, você precisa adicionar a instância do provedor ao IConfigurationBuilder . Você pode chamar o método Add de IConfigurationBuilder , mas eu lançarei imediatamente a lógica de inicialização YamlConfigurationProvider no método de extensão .
Implementar YamlConfigurationExtensions public static class YamlConfigurationExtensions { public static IConfigurationBuilder AddYaml( this IConfigurationBuilder builder, string filePath) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); return builder .Add(new YamlConfigurationSource(filePath)); } }
Chamar o método AddYaml public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); }
Alterar rastreamento
Na nova configuração da API , tornou-se possível reler a fonte de configuração quando ela foi alterada. Ao mesmo tempo, o aplicativo não é reiniciado.
Como funciona:
- O Provedor de configuração monitora as alterações na fonte de configuração
- Se ocorrer uma alteração na configuração, um novo IChangeToken será criado .
- Quando o IChangeToken é alterado , um recarregamento de configuração é chamado
Vamos ver como o rastreamento de alterações é implementado no FileConfigurationProvider .
ChangeToken.OnChange(
Dois parâmetros são passados para o método OnChange da classe estática ChangeToken . O primeiro parâmetro é uma função que retorna um novo IChangeToken quando a fonte de configuração (neste caso, o arquivo) muda, esse é o chamado produtor . O segundo parâmetro é a função de retorno de chamada (ou consumidor ), que será chamada quando a fonte de configuração for alterada.
Saiba mais sobre a classe ChangeToken .
Nem todos os provedores de configuração implementam o rastreamento de alterações. Esse mecanismo está disponível para os descendentes de FileConfigurationProvider e AzureKeyVaultConfigurationProvider .
Conclusão
No .NET Core, temos um mecanismo fácil e conveniente para gerenciar as configurações do aplicativo. Muitos complementos estão disponíveis imediatamente, muitas coisas são usadas por padrão.
Obviamente, cada pessoa decide qual caminho usar, mas sou a favor do fato de que as pessoas conhecem suas ferramentas.
Este artigo aborda apenas o básico. Além do básico, IOptions, scripts de pós-configuração, validação de configurações e muito mais estão disponíveis para nós. Mas isso é outra história.
Você pode encontrar o projeto do aplicativo com exemplos deste artigo no repositório no Github .
Compartilhe nos comentários quem usa quais abordagens de gerenciamento de configuração?
Obrigado pela atenção.
upd .: como o AdAbsurdum sugeriu corretamente, ao trabalhar com matrizes, os elementos nem sempre serão substituídos ao mesclar uma configuração de duas fontes.
Considere um exemplo. Ao ler uma matriz de appsettings.json , obtemos esta visualização plana:
array:0=valueA
Ao ler a partir de appsettings.Development.json :
array:0=valueB array:1=value
Como resultado, a configuração será:
array:0=valueB array:1=value
Todos os elementos com índices exclusivos ( matriz: 1 no exemplo) serão adicionados à matriz resultante. Elementos de diferentes origens de configuração, mas com o mesmo índice ( matriz: 0 no exemplo) serão mesclados e o elemento que foi adicionado por último será usado.