Скачать исходный код |
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-файлами:
Комментариев нет:
Отправить комментарий