Хабрахабр

[Из песочницы] Как работает конфигурация в .NET Core

Предлагаю поговорить о простом, об организации настроек приложения. Давайте отложим разговоры о DDD и рефлексии на время.

NET Core, возник вопрос, как организовать файлы конфигурации, как выполнять трансформации и пр. После того как мы с коллегами решили перейти на . Во многих примерах встречается следующий код, и многие его успешно используют. в новой среде.

public IConfiguration Configuration
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();
}

NET Core. Но давайте разберемся, как работает конфигурация, и в каких случаях использовать данный подход, а в каких довериться разработчикам . Прошу под кат.

Как было раньше

Одним из первых вопросов после перехода на ASP. Как и у любой истории, у этой статьи есть начало. NET Core были трансформации конфигурационных файлов.

Вспомним как это было ранее c web.config

Основным был файл web.config, и к нему уже применялись трансформации (web. Конфигурация состояла из нескольких файлов. При этом активно использовались xml-атрибуты для поиска и трансформации секции xml-документа. Development.config и др.) в зависимости от конфигурации сборки.

NET Core файл web.config заменен на appsettings.json и привычного механизма трансформаций больше нет. Но как мы знаем в ASP.

Что нам говорит google?

NET Core " в google стал следующий код: Результатом поиска " Трансформации в ASP.

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

При этом мы явно указываем какие источники конфигурации мы хотим использовать. В конструкторе класса Startup мы создаем объект конфигурации с помощью ConfigurationBuilder.

И такой:

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

В зависимости от переменной окружения выбирается тот или иной источник конфигурации.

Но не покидало ощущение. Данные ответы часто встречаются на SO и других менее популярных ресурсах. Как быть если я хочу использовать переменные окружения или аргументы командной строки в конфигурации? что мы идем не туда. Почему мне нужно писать этот код в каждом проекте?

И я хочу поделиться полученным знанием в данной статье. В поисках истины пришлось забраться вглубь документации и исходного кода.

NET Core. Давайте разберемся, как работает конфигурация в .

Конфигурация

NET Core представлена объектом интерфейса IConfiguration. Конфигурация в .

public interface IConfiguration
{ string this[string key] { get; set; } IConfigurationSection GetSection(string key); IEnumerable<IConfigurationSection> GetChildren(); IChangeToken GetReloadToken();
}

  • [string key] индексатор, который позволяет по ключу получить значение параметра конфигурации
  • GetSection(string key) возвращает секцию конфигурации, которая соответствует ключу key
  • GetChildren() возвращает набор подсекций текущей секции конфигурации
  • GetReloadToken() возвращает экземпляр IChangeToken, который можно использовать для получения уведомлений при изменении конфигурации

При чтении из источника конфигурации (файл, переменные окружения) иерархические данные приводятся к плоской структуре. Конфигурация представляет собой набор пар "ключ-значение". Например json-объект вида

{ "Settings": { "Key": "I am options" }
}

будет приведен к плоскому виду:

Settings:Key = I am options

Здесь ключом является Settings:Key, а значением I am options.
Для наполнения конфигурации используются провайдеры конфигурации.

Провайдеры конфигурации

За чтение данных из источника конфигурации отвечает объект интерфейса
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(string key, out string value) позволяет по ключу получить значение параметра конфигурации
  • Set(string key, string value) используется для установки значения параметра конфигурации
  • GetReloadToken() возвращает экземпляр IChangeToken, который можно использовать для получения уведомлений при изменении источника конфигурации
  • Load() метод который отвечает за чтение источника конфигурации
  • GetChildKeys(IEnumerable<string> earlierKeys, string parentPath) позволяет получить список всех ключей, которые предоставляет данный поставщик конфигурации

Из коробки доступны следующие провайдеры:

  • Json
  • Ini
  • Xml
  • Environment Variables
  • InMemory
  • Azure
  • Кастомный провайдер конфигурации

Приняты следующие соглашения использования провайдеров конфигурации.

  1. Источники конфигурации считываются в том порядке, в котором они были указаны
  2. Если в разных источниках конфигурации присутствуют одинаковые ключи (сравнение идет без учета регистра), то используется значение, которое было добавлено последним.

Если мы создаем экземпляр web-сервера используя CreateDefaultBuilder, то по умолчанию подключаются следующие провайдеры конфигурации:

  • ChainedConfigurationProvider через этот провайдер можно получать значения и ключи конфигурации, которые были добавлены другими провайдерами конфигурации
  • JsonConfigurationProvider использует в качестве источника конфигурации json-файлы. Как можно заметить, в список провайдеров добавлены три провайдера данного типа. Первый использует в качестве источника appsettings.json, второй appsettings.{environment}.json. Третий считывает данные из secrets.json. Если выполнить сборку приложения в конфигурации Release, третий провайдер не будет подключен, потому что не рекомендуется использовать секреты в Production-среде
  • EnvironmentVariablesConfigurationProvider получает параметры конфигурации из переменных окружения
  • CommandLineConfigurationProvider позволяет добавлять аргументы командой строки в конфигурацию

По умолчанию это работает так. Так как конфигурация хранится как словарь, то необходимо обеспечить уникальность ключей.

Если в провайдере CommandLineConfigurationProvider имеется элемент с ключом key и в провайдере JsonConfigurationProvider имеется элемент с ключом key, элемент из JsonConfigurationProvider будет заменен элементом из CommandLineConfigurationProvider так как он регистрируется последним и имеет больший приоритет.

Вспомним пример из начала статьи

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

Данный подход необходим в том случае, когда мы хотим ограничить количество источников конфигурации. Нам не нужно самим создавать IConfiguration, чтобы выполнить трансформацию файлов конфигурации, так как это включено по умолчанию.

Кастомный провайдер конфигурации

IConfigurationSource новый интерфейс, который мы еще не рассматривали в данной статье. Для того, чтобы написать свой поставщик конфигурации необходимо реализовать интерфейсы IConfigurationProvider и IConfigurationSource.

public interface IConfigurationSource
{ IConfigurationProvider Build(IConfigurationBuilder builder);
}

Интерфейс состоит из единственного метода Build, который принимает в качестве параметра IConfigurationBuilder и возвращает новый экземпляр IConfigurationProvider.

В этих классах уже реализована логика методов TryGet, Set, GetReloadToken, GetChildKeys и остается реализовать только метод Load. Для реализации своих поставщиков конфигурации нам доступны абстрактные классы ConfigurationProvider и FileConfigurationProvider.

Необходимо реализовать чтение конфигурации из yaml-файла, при этом также необходимо чтобы мы могли изменять конфигурацию без перезагрузки нашего приложения. Рассмотрим на примере.

Создадим класс YamlConfigurationProvider и сделаем его наследником FileConfigurationProvider.

public class YamlConfigurationProvider : FileConfigurationProvider
{ private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); }
}

Конструктор принимает экземпляр FileConfigurationSource, который содержит в себе IFileProvider. В приведенном фрагменте кода можно заметить некоторые особенности класса FileConfigurationProvider. Также можно заметить, что метод Load принимает Stream в котором открыт для чтения файл конфигурации. IFileProvider используется для чтения файла, и для подписки на событие изменения файла. Это метод класса FileConfigurationProvider и его нет в интерфейсе IConfigurationProvider.

Для чтения файла я воспользуюсь пакетом YamlDotNet. Добавим простую реализацию, которая позволит считать yaml-файл.

Реализация 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; } } }
}

Для создания экземпляра нашего провайдера конфигурации необходимо реализовать FileConfigurationSource.

Реализация 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); }
}

EnsureDefaults(builder). Тут важно отметить, что для инициализации свойств базового класса необходимо вызвать метод this.

Можно вызвать метод Add из IConfigurationBuilder, но я сразу вынесу логику инициализации YamlConfigurationProvider в extension-метод. Для регистрации кастомного провайдера конфигурации в приложении необходимо добавить экземпляр провайдера в IConfigurationBuilder.

Реализация 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)); }
}

Вызов метода 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>();
}

Отслеживание изменений

При этом не происходит перезапуска приложения.
Как это работает: В новом api-конфигурации появилась возможность перечитывать источник конфигурации при его изменении.

  • Поставщик конфигурации отслеживает изменение источника конфигурации
  • Если произошло изменение конфигурации, создается новый IChangeToken
  • При изменении IChangeToken вызывается перезагрузка конфигурации

Посмотрим как реализовано отслеживание изменений в FileConfigurationProvider.

ChangeToken.OnChange( //producer () => Source.FileProvider.Watch(Source.Path), //consumer () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); });

Первый параметр это функция которая возвращает новый IChangeToken при изменении источника конфигурации (в данном случае файла), это т.н producer. В метод OnChange статического класса ChangeToken передается два параметра. Вторым параметром идет функция-callback (или consumer), которая будет вызвана при изменении источника конфигурации.
Подробнее о классе ChangeToken.

Этот механизм доступен для потомков FileConfigurationProvider и AzureKeyVaultConfigurationProvider. Не все провайдеры конфигурации реализуют отслеживание изменений.

Заключение

NET Core у нас появился легкий, удобный механизм для управления настройками приложения. В . Много надстроек доступно из коробки, многие вещи используются по умолчанию.
Конечно каждый сам решает, какой способ ему использовать, но я за то, чтобы люди знали свои инструменты.

Помимо основ нам доступны IOptions, сценарии пост-конфигурации, валидация настроек и многое другое. Данная статья затрагивает лишь основы. Но это уже другая история.

Проект приложения с примерами из данной статьи вы можете найти в репозитории на Github.
Делитесь в комментариях, кто какие подходы по организации конфигурации использует?
Спасибо за внимание.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть