- PVSM.RU - https://www.pvsm.ru -
Давайте отложим разговоры о DDD и рефлексии на время. Предлагаю поговорить о простом, об организации настроек приложения.
После того как мы с коллегами решили перейти на .NET Core, возник вопрос, как организовать файлы конфигурации, как выполнять трансформации и пр. в новой среде. Во многих примерах встречается следующий код, и многие его успешно используют.
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();
}
Но давайте разберемся, как работает конфигурация, и в каких случаях использовать данный подход, а в каких довериться разработчикам .NET Core. Прошу под кат.
Как и у любой истории, у этой статьи есть начало. Одним из первых вопросов после перехода на ASP.NET Core были трансформации конфигурационных файлов.
Конфигурация состояла из нескольких файлов. Основным был файл web.config, и к нему уже применялись трансформации (web.Development.config и др.) в зависимости от конфигурации сборки. При этом активно использовались xml-атрибуты для поиска и трансформации секции xml-документа.
Но как мы знаем в ASP.NET Core файл web.config заменен на appsettings.json и привычного механизма трансформаций больше нет.
Результатом поиска " Трансформации в ASP.NET Core " в google стал следующий код:
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();
}
Конфигурация представляет собой набор пар "ключ-значение". При чтении из источника конфигурации (файл, переменные окружения) иерархические данные приводятся к плоской структуре. Например 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);
}
Из коробки доступны следующие провайдеры:
Приняты следующие соглашения использования провайдеров конфигурации.
Если мы создаем экземпляр web-сервера используя CreateDefaultBuilder, то по умолчанию подключаются следующие провайдеры конфигурации:
Так как конфигурация хранится как словарь, то необходимо обеспечить уникальность ключей. По умолчанию это работает так.
Если в провайдере 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, чтобы выполнить трансформацию файлов конфигурации, так как это включено по умолчанию. Данный подход необходим в том случае, когда мы хотим ограничить количество источников конфигурации.
Для того, чтобы написать свой поставщик конфигурации необходимо реализовать интерфейсы IConfigurationProvider и IConfigurationSource. IConfigurationSource новый интерфейс, который мы еще не рассматривали в данной статье.
public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}
Интерфейс состоит из единственного метода Build, который принимает в качестве параметра IConfigurationBuilder и возвращает новый экземпляр IConfigurationProvider.
Для реализации своих поставщиков конфигурации нам доступны абстрактные классы ConfigurationProvider и FileConfigurationProvider. В этих классах уже реализована логика методов TryGet, Set, GetReloadToken, GetChildKeys и остается реализовать только метод Load.
Рассмотрим на примере. Необходимо реализовать чтение конфигурации из 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();
}
}
В приведенном фрагменте кода можно заметить некоторые особенности класса FileConfigurationProvider. Конструктор принимает экземпляр FileConfigurationSource, который содержит в себе IFileProvider. IFileProvider используется для чтения файла, и для подписки на событие изменения файла. Также можно заметить, что метод Load принимает Stream в котором открыт для чтения файл конфигурации. Это метод класса FileConfigurationProvider и его нет в интерфейсе IConfigurationProvider.
Добавим простую реализацию, которая позволит считать yaml-файл. Для чтения файла я воспользуюсь пакетом YamlDotNet.
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.
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);
}
}
Тут важно отметить, что для инициализации свойств базового класса необходимо вызвать метод this.EnsureDefaults(builder).
Для регистрации кастомного провайдера конфигурации в приложении необходимо добавить экземпляр провайдера в IConfigurationBuilder. Можно вызвать метод Add из IConfigurationBuilder, но я сразу вынесу логику инициализации YamlConfigurationProvider в extension-метод.
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));
}
}
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-конфигурации появилась возможность перечитывать источник конфигурации при его изменении. При этом не происходит перезапуска приложения.
Как это работает:
Посмотрим как реализовано отслеживание изменений в FileConfigurationProvider.
ChangeToken.OnChange(
//producer
() => Source.FileProvider.Watch(Source.Path),
//consumer
() => {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
В метод OnChange статического класса ChangeToken передается два параметра. Первый параметр это функция которая возвращает новый IChangeToken при изменении источника конфигурации (в данном случае файла), это т.н producer. Вторым параметром идет функция-callback (или consumer), которая будет вызвана при изменении источника конфигурации.
Подробнее о классе ChangeToken [1].
Не все провайдеры конфигурации реализуют отслеживание изменений. Этот механизм доступен для потомков FileConfigurationProvider и AzureKeyVaultConfigurationProvider.
В .NET Core у нас появился легкий, удобный механизм для управления настройками приложения. Много надстроек доступно из коробки, многие вещи используются по умолчанию.
Конечно каждый сам решает, какой способ ему использовать, но я за то, чтобы люди знали свои инструменты.
Данная статья затрагивает лишь основы. Помимо основ нам доступны IOptions, сценарии пост-конфигурации, валидация настроек и многое другое. Но это уже другая история.
Проект приложения с примерами из данной статьи вы можете найти в репозитории на Github [2].
Делитесь в комментариях, кто какие подходы по организации конфигурации использует?
Спасибо за внимание.
Автор: Qdimka
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/318760
Ссылки в тексте:
[1] ChangeToken: https://docs.microsoft.com/ru-ru/aspnet/core/fundamentals/change-tokens?view=aspnetcore-2.2
[2] Github: https://github.com/qdimka/netcore-configuration-sample
[3] Источник: https://habr.com/ru/post/453416/?utm_campaign=453416
Нажмите здесь для печати.