Continuous Integration: Рефакторинг Config-файлов

2 июня 2013 г.

Скачать исходный код

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

До того, как наш релиз попадет на боевой сервер, он обычно разворачивается сначала на окружении для разработки, потом на staging или preproduction, а уже потом идет на сервер с конечными пользователями. Для каждого из этих окружений нужно выставить свои настройки подключения к БД, SMTP-сервера, пути в файловой системе и т.п.

В примерах я буду писать про App.config, но всё описанное будет применимо и для Web.config, и для любого config-файла, основаного на XML.

Отдельный Config-файл на окружение

Одним из способов хранения конфигураций является создание config-файлов для каждого окружения. Т.е. мы создаем файлы App.Debug.config, App.Staging.config и App.Release.config, которые полностью копируют друг друга за исключением нескольких настроек специфичных для конкрентного окружения.

Во время сборки проекта создается файл App.config на основе одного из файлов App.*.config в зависимости от выбранной конфигурации. Код для создания может выглядеть так:

<Target Name="BeforeBuild">
  <Copy SourceFiles="App.$(Configuration).config" DestinationFiles="App.config" />
</Target>

Дублирование кода

Этот подход решает проблему хранения разных конфигураций, но у него есть существенный недостаток. Большая часть настроек в файлах App.*.config дублируется, отличаются только некоторые части, которые специфичны для конкретной конфигурации. Мы знаем к чему ведет дублирование в коде, поэтому стоит рассмотреть способ, в котором дублирование устранено.

Общий Config-файл и несколько трансформаций

В предыдущем подходе мы поняли, что будем дублировать слишком много XML-кода. Чтобы этого избежать, мы вынесем все общие части в общий файл конфигурации App.config. Переменные части будут вынесены в файлы трансформации. Этот способ чем-то напоминает шаблон Template Method, только в качестве механизма полиморфизма у нас будет работать TransformXml.

Мы подробно рассматривали, как работают трансфомации для Web.config и трансформации для App.config, поэтому сейчас на этом останавливаться не будем.

Внешние конфигурации

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

Для примера возьмем настройку логирования с помощью утилиты log4net. Типовая конфигурация log4net в App.config будет выглядеть так:

<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <!-- ... -->
  <log4net>
    <root>
      <level value="Info" />
      <appender-ref ref="ConsoleAppender" />
      <appender-ref ref="RollingFileAppender" />
    </root>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level - %message%newline" />
      </layout>
    </appender>
 <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="log.txt" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="1000" />
      <maximumFileSize value="1000KB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%property{log4net:HostName}%newline%date [%thread] %-5level %logger %newline %appdomain %newline %message%newline%newline%newline" />
      </layout>
    </appender>
  </log4net>
  <!-- ... -->
</configuration>

В нашем проекте может быть десяток сервисов, несколько веб-приложений и других утилит. Если мы захотим для каждой из них использовать log4net, то нам придется во все App.config и Web.config добавлять секцию <log4net> с одинаковым кодом. Это выглядит явным дублированием кода. Особенно остро мы ощутим проблему, когда заходим что-то поменять в настройках логирования. Нам придется найти все места, где был сконфигурирован log4net и внести изменения.

Атрибут configSource

В .NET для секций Config-файла можно указать configSource. Это ссылка на внешний файл, где содержится определение данной секции. В общем виде это выглядит так:

<pages configSource="ConfigFolder\commonPages.config"/>

Т.е. в самом файле App.config секция pages определена не будет. В момент запуска приложения .NET считает данные для этой секции из файла commonPages.config.

В случае с log4net мы получаем следущие файлы:

App.config:

<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <!-- ... -->
  <log4net configSource="log4netConfiguration.config" />
</configuration>

log4netConfiguration.config:

<log4net>
    <root>
      <level value="Info" />
      <appender-ref ref="RollingFileAppender" />
      <appender-ref ref="ConsoleAppender" />
    </root>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="DEBUG MODE %date [%thread] %-5level %message" />
      </layout>
    </appender>
    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
      <file value="log.txt" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="1000" />
      <maximumFileSize value="1000KB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="DEBUG MODE %date [%thread] %-5level %message" />
      </layout>
    </appender>
 </log4net>

В каждом файле конфигурации мы осталяем только ссылку на секцию log4net, сама секция будет определена во внешнем файле log4netConfiguration.config. Получилось, что при изменении настроек логирования, мы внесем изменения в одном месте и все наши приложения начнут это использовать.

При указании пути до файла в атрибуте configSource есть ограничение, нельзя указывать относительные пути на папки выше текущей. Внешний файл должен быть в том же каталоге или подкаталоге, что и App.config.

Чтобы это обойти, есть простой и неправильный способ, мы можем скопировать файл log4netConfiguration.config в каждый проект. Такой способ ведет к дублированию и я не рекомендую его использовать.

Правильным решением будет добавление log4netConfiguration.config в проект как ссылки и указанием ему копироваться по время сборки проекта:

Мы получили один файл, где сконфигурирован log4net и с помощью configSource подключаем его к другим конфигам.

Внешняя конфигурация на каждое окружение

Продолжим пример с настройками логирования. У меня в проекте была ситуация, когда логирование должно было настраиваться по-разному в зависимости от окружения. Для решения этой задачи применяется стандартный подход с файлами трансформации. Мы создаем трансформации log4netConfiguration.*.config, в которых описываем трансформации под каждое окружение. Для примера файл log4netConfiguration.UAT.config:

<log4net xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appender>
    <layout>
      <conversionPattern xdt:Transform="Replace" value="UAT %date [%thread] %-5level %message" />
    </layout>
  </appender>
</log4net>

В этой трансформации мы заменяем шаблон отображения сообщения и добавляем в него слово UAT. По примеру можно менять БД для логирования, адреса СМС-серверов и т.п. Для применения этой трансформации добавим необходимый код в ConsoleApp.csproj:

  <Target Name="AfterBuild">
    <TransformXml Source="$(SolutionDir)config\log4netConfiguration.config" 
                   Transform="$(SolutionDir)config\log4netConfiguration.$(Configuration).config" 
                   Destination="$(OutDir)log4netConfiguration.config" StackTrace="true" />
  </Target>

Запустим консольное приложение и увидим, что логирование ведется с настройками для UAT-окружения:

Внешние конфигурации секции appSettings

Секция appSetting обладает атрибутом file. С помощью него можно указать внешний файл конфигурации, где будут описаны ключи для этой секции. Отличие от configSource заключается в том, что секция appSettings может быть не пустой, мы можем доопределять ключи. Так будут выглядеть внешний файл конфигурации и App.config:

App.config:

<configuration>
  <appSettings file="commonAppSettings.config">
    <add key="SmtpUserName" value="username-smtp" />
  </appSettings>
</configuration>

commonAppSettings.config:

<appSettings>
 <add key="SmtpHost" value="common.smtp.host" />
 <add key="Profile" value="On" />
</appSettings>

В App.config определен только 1 ключ, в commonAppSettings.config еще 2 ключа. Запускаем проект и видим, что считались все 3 ключа:

Итого

Файлы конфигурации можно и нужно рефакторить, не допуская дублирования в XML-коде. Мы рассмотрели основные приемы: трансформация, использование configSource и атрибута file. Исходный код приведенных примеров вы можете скачать на github.


Статья из серии Continuous Integration: Работа с Config-файлами:

  1. Continuous Integration: Трансформация Web.config
  2. Continuous Integration: Процесс сборки проекта и трансформации Config-файлов
  3. Continuous Integration: Создание собственной конфигурации
  4. Continuous Integration: Трансформация App.config
  5. Continuous Integration: Рефакторинг файлов конфигурации

Комментариев нет:

Отправить комментарий

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...