ФЕВРАЛЯ 2016

ТОМ 31 НОМЕР 2

Главное в .NET - Конфигурация в .NET Core

Марк Михейлис | ФЕВРАЛЯ 2016

Mark MichaelisТе, кто работал с ASP.NET 5, несомненно заметили поддержку новой конфигурации, включенную в эту платформу и доступную в наборе NuGet-пакетов Microsoft.Extensions.Configuration. Новая конфигурация разрешает использовать список пар «имя-значение», которые можно группировать в многоуровневую иерархию. Так, один параметр вы можете хранить в SampleApp:Users:InigoMontoya:MaximizeMainWindow, а другой — в SampleApp:AllUsers:Default:MaximizeMainWindow. Любое сохраненное значение преобразуется в строку, и встроенная поддержка связывания позволяет вам десериализовать параметры в пользовательский POCO-объект. Разработчики, уже знакомые с новым API конфигурирования, по-видимому, впервые встретили его в ASP.NET 5. Но этот API ни в коей мере не ограничен ASP.NET. По сути, все листинги в этой статье были созданы в проекте Unit Testing в Visual Studio 2015 с использованием Microsoft .NET Framework 4.5.1, которая ссылается на пакеты Microsoft.Extensions.Configuration из ASP.NET 5 RC1. (Исходный код вы найдете по ссылке gitHub.com/IntelliTect/Articles)

API конфигурирования поддерживает провайдеры конфигураций для находящихся в памяти .NET-объектов, INI-, JSON- и XML-файлов, аргументов командной строки, переменных окружения, зашифрованного хранилища пользователя, а также любой пользовательский провайдер, созданный вами. Если вы хотите задействовать JSON-файлы для своей конфигурации, просто добавьте NuGet-пакет Microsoft.Extensions.Configuration.Json. Затем, если вам требуется разрешить командной строке предоставлять конфигурационную информацию, добавьте NuGet-пакет Microsoft.Extensions.Configuration.CommandLine либо в дополнение к другим конфигурационным ссылкам, либо вместо них. Если вам не подходит ни один из встроенных провайдеров конфигураций, вы можете создать собственный провайдер, реализовав интерфейсы, находящиеся в Microsoft.Extensions.Configuration.Abstractions.

Получение параметров конфигурации

Чтобы ознакомиться с получением конфигурационных параметров, взгляните на рис. 1.

Рис. 1. Базовая настройка с помощью методов расширения InMemoryConfigurationProvider и 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();

      // Добавляем defaultConfigurationStrings
      configurationBuilder.AddInMemoryCollection(
        DefaultConfigurationStrings);
      Configuration = configurationBuilder.Build();

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

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

Обращение к конфигурации начинается с создания экземпляра ConfigurationBuilder — класса, доступного в NuGet-пакете Microsoft.Extensions.Configuration. Располагая экземпляром ConfigurationBuilder, вы можете добавлять провайдеры, напрямую используя методы расширения IConfigurationBuilder вроде AddInMemoryCollection, показанного на рис. 1. Этот метод принимает экземпляр Dictionary<string,string>, содержащий пары «имя-значение» конфигурации, и использует его для инициализации провайдера конфигурации до его добавления к экземпляру ConifigurationBuilder. После того как формирователь конфигурации (configuration builder) «сконфигурирован», вы вызываете его метод Build, чтобы получить конфигурацию.

Как упоминалось ранее, конфигурация — это просто иерархический список пар «имя-значение», узлы в котором разделяются двоеточием. Поэтому, чтобы получить конкретное значение, вы просто обращаетесь к индексатору Configuration с ключом соответствующего элемента:

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

Однако доступ к значению не ограничивается только получением строк. Вы можете, например, получить значения через методы расширения Get<T> для ConfigurationBinder. Скажем, чтобы получить размер экранного буфера для основного окна, используйте:

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

Поддержка этой привязки требует ссылки на NuGet-пакет Microsoft.Extensions.Configuration.Binder.

Обратите внимание на то, что имеется необязательный аргумент, следующий за ключом, для которого можно указать значение по умолчанию, возвращаемое, когда данный ключ не существует. (Без этого значения по умолчанию будет возвращаться default(T), а не генерироваться исключение, как вы могли ожидать.)

Конфигурационные значения могут быть не только скалярными. Вы можете получать POCO-объекты или даже целые графы объектов. Чтобы извлечь экземпляр ConsoleWindow, члены которого сопоставляются с разделом AppConfiguration:MainWindow конфигурации, на рис. 1 используется следующий код:

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

В качестве альтернативы вы могли бы определить граф конфигурации вроде AppConfiguration, как на рис. 2.

Рис. 2. Пример объектного графа конфигурации

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

  // Требует ссылки на System.Diagnostics.TraceSource в Corefx
  System.Diagnostics.Trace.Assert(
    600 == appConfiguration.MainWindow.Width);
}

С помощью такого графа объектов вы могли бы определить всю конфигурацию (или ее часть) как иерархию строго типизированных объектов, которую потом можно было бы использовать для получения всех параметров одним махом.

Несколько провайдеров конфигурации

InMemoryConfigurationProvider эффективен для хранения значений по умолчанию или, возможно, вычисляемых значений. Но при наличии только этого провайдера вы взваливаете на себя бремя получения конфигурации и загрузки ее в Dictionary<string,string> до регистрации в ConfigurationBuilder. К счастью, существует еще несколько встроенных провайдеров конфигурации, в том числе три провайдера, работающих с файлами (XmlConfigurationProvider, IniConfigurationProvider и JsonConfigurationProvider), провайдер, работающий с переменными окружения (EnvironmentVariableConfigurationProvider), и провайдер аргументов командной строки (CommandLineConfigurationProvider). Более того, эти провайдеры можно комбинировать так, как это подходит для логики вашего приложения. Вообразите, к примеру, что вам нужно указать параметры конфигурации в следующем порядке возрастания приоритета:

  • InMemoryConfigurationProvider;
  • JsonFileConfigurationProvider для Config.json;
  • JsonFileConfigurationProvider для Config.Production.json;
  • EnvironmentVariableConfigurationProvider;
  • CommandLineConfigurationProvider.

Иначе говоря, значения конфигурации по умолчанию можно хранить в коде. Затем файл config.json и следом за ним файл Config.Production.json могут переопределять значения, указанные в InMemory, т. е. позднее добавленные провайдеры (например, относящиеся к JSON) имеют приоритет над ранее добавленными провайдерами и переопределяют любые перекрывающиеся значения. Впоследствии при развертывании у вас могут быть собственные значения конфигурации, хранящиеся в переменных окружения. Скажем, вместо «зашивки» в Config.Production.json определенный параметр среды можно извлекать из переменной окружения Windows и обращаться к файлу (допустим, к Config.Test.Json), указанному в этой переменной окружения. (Простите меня за неоднозначность термина «параметр среды», относящегося к производственной, тестовой, опытной среде или к среде разработки, в сравнении с переменными окружения Windows, такими как %USERNAME% или %USERDOMAIN%.) Наконец, вы указываете (или переопределяете) любые ранее предоставленные параметры через командную строку, например как разовое изменение для включения протоколирования.

Чтобы указать каждый из провайдеров, добавьте их в формирователь конфигурации (через метод расширения AddX текучего API), как показано на рис. 3.

Рис. 3. Добавление нескольких провайдеров конфигурации: указанный последним имеет наивысший приоритет

public static void Main(string[] args = null)
{
  ConfigurationBuilder configurationBuilder =
    new ConfigurationBuilder();

  configurationBuilder
    .AddInMemoryCollection(DefaultConfigurationStrings)
    .AddJsonFile("Config.json",
    true) // это булево значение указывает,
          // что файл не обязателен
    // EssentialDotNetConfiguartion – необязательный префикс
    // для всех ключей конфигурации среды, но после его
    // использования будут обнаруживаться лишь переменные
    // окружения с этим префиксом
    .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);
}

В случае JsonConfigurationProvider вы можете либо требовать наличия файла, либо сделать его не обязательным; отсюда и дополнительный (не обязательный) параметр в AddJsonFile. Если параметра нет, файл требуется и в случае его отсутствия будет генерироваться System.IO.FileNotFoundException. Учитывая иерархическую природу JSON, эта конфигурация отлично соответствует API конфигурирования (рис. 4).

Рис. 4. Данные конфигурации JSON для 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"
  }
}

CommandLineConfigurationProvider требует указывать аргументы при регистрации в формирователе конфигурации. Аргументы задаются строковым массивом пар «имя-значение», при этом каждая пара имеет формат /<name>=<value>, где знак равенства обязателен. Ведущий слеш тоже обязателен, но второй параметр функции AddCommandLine(string[] args, Dictionary<string,string> switchMappings) позволяет предоставлять псевдонимы, которые нужно предварять либо -, либо --. Например, словарь значений разрешит командной строке «program.exe -LogFile="c:\programdata\Application Data\Program.txt"» загружать в элемент конфигурации AppConfiguration:LogFile:

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

Прежде чем закончить с основами конфигурации, отметим несколько дополнительных моментов.

  • CommandLineConfigurationProvider имеет несколько характеристик, которые не являются интуитивно понятным из IntelliSense и о которых вы должны знать:
    • switchMappings в CommandLineConfigurationProvider допускает для ключей лишь два префикса: - или --. Даже слеш (/) недопустим в качестве параметра ключа. Это не дает вам предоставлять псевдонимы для слешей ключей через сопоставления ключей;
    • CommandLineConfigurationProviders запрещает аргументы командной строки на основе ключей, т. е. аргументы, не включающие присвоенное значение. Например, задание ключа в виде «/Maximize» недопустимо;
    • хотя вы можете передавать аргументы Main новому экземпляру CommandLineConfigurationProvider, передавать Environment.GetCommandLineArgs без предварительного удаления имени процесса нельзя. Заметьте, что Environment.GetCommandLineArgs ведет себя иначе, когда подключается отладчик. А именно: в отсутствие отладчика имена исполняемых файлов с пробелами разбиваются на индивидуальные аргументы. См. страницу itl.ty\GetCommandLineGotchas;
    • при указании префикса ключа - или --, для которого нет соответствующего сопоставления ключа, генерируется исключение.
  • Хотя конфигурации можно обновлять (Configuration["Profile:UserName"]="Inigo Montoya"), обновленное значение не сохраняется обратно в исходном хранилище. Например, когда вы присваиваете конфигурационное значение провайдеру JSON, JSON-файл не обновляется. Аналогично переменная окружения не будет обновляться, когда вы присвоите ей элемент конфигурации.
  • EnvironmentVariableConfigurationProvider дополнительно (не обязательно) позволяет указывать префикс ключа. В таких случаях он будет загружать только те переменные окружения, которые имеют заданный префикс. Тем самым вы можете автоматически ограничивать элементы конфигурации теми, которые находятся в разделе переменных окружения или, если шире, теми, которые релевантны для вашего приложения.
  • Поддерживаются переменные окружения с разделителем в виде двоеточия. Например, в командной строке допускается присваивание SET AppConfiguration:ConnectionString=Console.
  • Все ключи (имена) нечувствительны к регистру букв.
  • Каждый провайдер содержится в собственном NuGet-пакете, причем имя NuGet-пакета соответствует названию провайдера: Microsoft.Extensions.Configuration.CommandLine, Microsoft.Extensions.Configuration.EnvironmentVariables, Microsoft.Extensions.Configuration.Ini, Microsoft.Extensions.Configuration.Json и Microsoft.Extensions.Configuration.Xml.

Вникаем в объектно-ориентированную структуру

Модульность и объектно-ориентированная структура API конфигурирования тщательно продуманы, предоставляя модульные и легко расширяемые классы и интерфейсы (рис. 5).

Configuration Provider Class Model
Рис. 5. Модель класса провайдера конфигурации

Каждый тип механизма конфигурирования имеет соответствующий класс провайдера конфигурации, который реализует интерфейс IConfigurationProvider. В большинстве встроенных провайдеров реализация первым делом начинается с наследования от ConfigurationBuilder, а не с использования своих реализаций для всех методов интерфейса. Возможно, это удивит вас, но на рис. 1 нет ни одной прямой ссылки ни на какой провайдер. Дело в том, что вместо ручного создания экземпляра каждого провайдера и его регистрации с помощью метода Add класса ConfigurationBuilder каждый NuGet-пакет провайдера включает статический класс расширения с методами расширения IConfigurationBuilder. (Имя класса расширения обычно идентифицируется по суффиксу ConfigurationExtensions.) С помощью классов расширения вы можете сразу же обращаться к конфигурационным данным непосредственно из ConfigurationBuilder (который реализует IConfigurationBuilder) и прямо вызывать метод расширения, связанный с вашим провайдером. Например, класс JasonConfigurationExtensions добавляет методы расширения AddJsonFile в IConfigurationBuilder, чтобы вы могли добавлять JSON-конфигурацию вызовом ConfigurationBuilder.AddJsonFile(fileName, optional).Build();.

После создания конфигурации, как правило, у вас есть все, что нужно для получения значений.

IConfiguration включает индексатор строк, позволяющий получать любое конкретное значение конфигурации, используя ключ для доступа к элементу, который вы ищете. Вы можете получить целый набор параметров (называемый разделом) вызовом метода GetSection или GetChildren (в зависимости от того, хотите ли вы спуститься на дополнительный уровень ниже в иерархии). Заметьте, что разделы конфигурационных элементов позволяют получать следующее:

  • ключ — последний элемент имени;
  • путь — полный путь от корня до текущего места;
  • значение — конфигурационное значение, хранящееся в конфигурационном параметре;
  • значение как объект — через ConfigurationBinder можно получить POCO-объект, соответствующий разделу конфигурации, к которому вы обращаетесь (и потенциально его дочерние элементы). Например, именно так работает Configuration.Get<AppConfiguration>(nameof(App­Configuration)) на рис. 3;
  • IConfigurationRoot — включает функцию Reload, позволяющую заново загружать значения, чтобы обновить конфигурацию. ConfigurationRoot (который реализует IConfigurationRoot) содержит метод GetReloadToken, дающий возможность регистрироваться на уведомления о том, когда происходит загрузка заново (и интересующее вас значение может измениться).

Зашифрованные параметры

Иногда вам будет необходимо получать параметры, которые зашифрованы, а не хранятся открытым текстом. Это важно, например, когда вы сохраняете OAuth-ключи или маркеры приложения или записываете удостоверения для строки подключения к базе данных. К счастью, система Microsoft.Extensions.Configuration имеет встроенную поддержку для чтения зашифрованных значений. Чтобы обратиться к защищенному хранилищу, нужно добавить ссылку на NuGet-пакет Microsoft.Extensions.Configuration.UserSecrets. После этого вы получите новый метод расширения IConfigurationBuilder.AddUserSecrets, который принимает строковый аргумент элемента конфигурации, userSecretsId (хранящийся в вашем файле project.json). Как и следовало ожидать, после добавления конфигурации UserSecrets к вашему формирователю конфигурации вы можете приступить к извлечению зашифрованных значений, к которым могут обращаться только пользователи, сопоставленные с этими настройками.

Очевидно, что получение параметров несколько бессмысленно, если вы не в состоянии устанавливать их значения. Для этого используйте утилиту user-secret.cmd:

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

Ключ --project позволяет сопоставить параметр со значением userSecretsId, хранящимся в вашем файле project.json (создаваемым по умолчанию мастером нового проекта ASP.NET 5). Если у вас нет утилиты user-secret, добавьте ее через командную строку разработчика, используя утилиту DNX (в настояще время — dnu.exe).

Подробнее о ключах настройки пользовательского секрета см. в статье Рика Андерсона (Rick Anderson) и Дэвида Рота (David Roth) «Safe Storage of Application Secrets» (bit.ly/1mmnG0L).

Заключение

Те, кто уже какое-то время используют .NET, вероятно, будут разочарованы встроенной поддержкой конфигурирования через System.Configuration. По-видимому, это особенно верно, если вы переходите с традиционной ASP.NET, где конфигурация была ограничена файлом Web.config или App.config и последующим обращением только к узлу AppSettings внутри одного из этих файлов. К счастью, новый Microsoft.Extensions.Configuration API с открытым исходным кодом идет гораздо дальше того, что было изначально доступно, добавляя множество новых провайдеров конфигурации наряду с легко расширяемой системой, к которой можно подключить какой угодно собственный провайдер. Для тех, кто все еще живет (застрял?) в мире без ASP.NET 5, по-прежнему работают старые System.Configuration API, но вы можете постепенно переходить на новый API (даже параллельно используя старый и новый API), просто ссылаясь на новые пакеты. Более того, NuGet-пакеты можно использовать из таких проектов Windows-клиентов, как консольные и WPF-приложения (Windows Presentation Foundation). Поэтому в следующий раз, когда вам понадобится доступ к конфигурационным данным, применяйте Microsoft.Extensions.Configuration API.

Исходный код можно скачать по ссылке GitHub.com/IntelliTect/Articles.


Марк Михейлис (Mark Michaelis) - учредитель IntelliTect, где является главным техническим архитектором и тренером. Почти два десятилетия был Microsoft MVP и региональным директором Microsoft с 2007 года. Работал в нескольких группах рецензирования проектов программного обеспечения Microsoft, в том числе C#, Microsoft Azure, SharePoint и Visual Studio ALM. Выступает на конференциях разработчиков, автор множества книг, последняя из которых — «Essential C# 6.0 (5th Edition)» itl.tc/EssentialCSharp). С ним можно связаться в Facebook facebook.com/Mark.Michaelis, через его блог IntelliTect.com/Mark, в Twitter @markmichaelis или по электронной почт mark@IntelliTect.com.

Выражаю благодарность за рецензирование статьи экспертам IntelliTect Гранту Эриксону (Grant Erickson), Дереку Говарду (Derek Howard), Филу Спокасу (Phil Spokas) и Майклу Стоуксбери (Michael Stokesbary).