Вводная

Это небольшая серия статей про новые стандартные механизмы в .NET. Вместе с выпуском .NET Standard Microsoft выпустили большое количество обвязок, которые должны привести в порядок некоторый зоопарк используемых технологий.

В первой статье (IoC-контейнер) мы поговорили про то, почему это круто — иметь реализацию DI-контейнера по умолчанию, и почему в большинстве случаев стоит использовать именно его. Также написали простую реализацию биндинга по атрибутам.

В этой — второй — я расскажу про недостатки старого способа конфигурирования приложения (через app.config/web.config) и про то, как они исправлены в новом подходе.

В третьей (Логирование) посмотрим на стандартный интерфейс логирования и прикрутим к нему привычный NLog.

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

План

  1. IoC-контейнер.
  2. Конфигурация.
  3. Логирование.

Текущая статья посвящена второй теме плана — Конфигурация.

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

Порядок в конфигурации — это то, чего всегда не хватало .NET приложениям. Стандартным механизмом был огромный xml-файл (*.config). Чем он плох?

  1. Соседство пользовательских настроек (appSettings, connectionStrings) с системными (bindingRedirect). Эта проблема решалась вынесением секций в отдельные файлы, но это не самый удобный подход.
  2. Далеко не все настройки можно  наглядно описать в appSettings. Очень часто хочется иерархии настроек. Эта проблема решалась написанием своих секций. Но на своих секциях не работают дефолтные вещи вроде шифрования (aspnet_regiis), для этого приходится долго бить в бубен. Свои секции — это странные объекты, отнаследованные от ConfigurationSection, что очень некрасиво смотрится.
  3. Трансформации для разных сред (Staging, Production) выполнять можно, но очень неудобно — существует отдельная MsBuild таска, которая выполняет трансформации. Еще, конечно, можно делать руками XSLT-преобразования, но это совсем не то, чем хочется заниматься, программируя приложение.

Что же нам предложили взамен? Гораздо более гибкий и удобный механизм Microsoft.Extensions.Configuration. Чтобы далеко не ходить, сразу перейду к коду. Сначала попробуем сконфигурировать приложение из кода, не используя файлы и кастомные провайдеры.

Создаем консольное приложение .NET Core и устанавливаем пакет:

Install-Package Microsoft.Extensions.Configuration.Abstractions -Version 1.1.2

Значения конфигураций хранятся по ключам. Ключ представляет собой строку, разделенную символом «:».

var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource
{
    InitialData = new List<KeyValuePair<string, string>>
    {
        new KeyValuePair<string, string>("App:Name", "MySuperApp"),
        new KeyValuePair<string, string>("App:BuildDate", "2017-07-13T10:02:02"),
        new KeyValuePair<string, string>("User:Name", "Julious"),
        new KeyValuePair<string, string>("User:BirthDate", "2017-07-13T00:00:00"),
    }
}).Build();

Теперь можем получить значения.

var appName = config.GetSection("App")["Name"];
var userName = config["User:Name"];

Это основной функционал. Но еще конфигурацию можно мапить в POCO-объекты.
Установим:

Install-Package Microsoft.Extensions.Configuration.Binder -Version 1.1.2

И сможем вызывать метод Get…

public class AppConfiguration
{
    public AppSettings App { get; set; }
    public UserSettings User { get; set; }

    public class AppSettings
    {
        public string Name { get; set; }
        public DateTime BuildDate { get; set; }
    }

    public class UserSettings
    {
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
    }
}
var appConfig = config.Get<AppConfiguration>();

…и в appConfig получим правильное дерево значений.
Ключи конфига нечуствительны к регистру, «user:name» эквивалентно «User:Name».

У этого подхода есть одна важная особенность: данные по ключам перезаписываются. То есть, добавив конфигурацию в билдер, мы перезапишем значения по совпадающим ключам, что позволяет нам очень здорово управлять тем, как наше приложение будет сконфигурировано.

Посмотрим, как это работает, на примере стандартных провайдеров. Установим пакеты:

Install-Package Microsoft.Extensions.Configuration.Json
Install-Package Microsoft.Extensions.Configuration.CommandLine
Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables

и напишем такой код

class Program
{
    public class AppConfiguration
    {
        public AppSettings App { get; set; }
        public UserSettings User { get; set; }

        public class AppSettings
        {
            public string Name { get; set; }
            public string Version { get; set; }
        }

        public class UserSettings
        {
            public string Name { get; set; }
            public DateTime BirthDate { get; set; }
            public string Fio { get; set; }
        }
    }

    static void Main(string[] args)
    {
        var initialSettings = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("App:Name", "AppNameFromMemory"),
            new KeyValuePair<string, string>("App:Version", "VersionFromMemory"),
            new KeyValuePair<string, string>("user:name", "NameFromMemory"),
            new KeyValuePair<string, string>("user:fio", "FioFromMemory"),
            new KeyValuePair<string, string>("User:BirthDate", "2017-07-13T00:00:00"),
        };

        var appConfig = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddInMemoryCollection(initialSettings)
            .AddJsonFile("appsettings.json", optional: true)
            .AddJsonFile("appsettings.dev.json", optional:true)
            .AddEnvironmentVariables("MyAppPrefix_")
            .AddCommandLine(args)
            .Build()
            .Get<AppConfiguration>();
                    
        Console.WriteLine($"App:Name={appConfig.App.Name}");
        Console.WriteLine($"App:Version={appConfig.App.Version}");
        Console.WriteLine($"User:Name={appConfig.User.Name}");
        Console.WriteLine($"User:Fio={appConfig.User.Fio}");
        Console.WriteLine($"User:BirthDate={appConfig.User.BirthDate}");

        Console.ReadLine();
    }
}

SetBasePath — устанавливает, где искать файлы, если пути относительные

optional — если false и файл не найден, то генерируется исключение

AddEnvironmentVariables(prefix) — размапит только те переменные среды, которые начинаются с этого префикса. Например, MyAppPrefix_User:Name будет записано по ключу User:Name.

Добавим конфигурационные файлы.

appsettings.json

{
  "User": {
    "Fio": "FioFromJson",
    "Name": "NameFromJson"
  },
  "App": {
    "Version": "VersionFromJson" 
  } 
}

appsettings.dev.json

{
  "User": {
    "Fio": "FioFromDevJson"
  }
}

Откроем свойства проекта и проставим такие значения:

Вывод программы после запуска:

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

В итоге, у Microsoft получилось простое и функциональное API для конфигурирования приложений, в котором учтены все проблемы старого подхода с app/web.config. Теперь не нужно писать свои секции для сложных настроек, не нужно трансформировать конфиг, в конфигах очень просто разобраться. Стандартных провайдеров почти всегда хватает за глаза, а если нет — то всегда можно написать свой и спокойно встроить его в общий механизм.

С первой и третьей частями цикла можно ознакомиться по ссылкам.

  1. IoC-контейнер — первая часть цикла.
  2. Логирование — третья часть цикла.