Denis Gladkikh

outcoldman

My personal blog about software development

  • 26 Aug 2010
  • ASP.NET 4, XDT, XML-Document-Transform, Config Transformation Tool

В ASP.NET 4 появилась фича Web.Config Transfomration, на первый взляд очень полезная и интересная вещь для развертывания веб-приложений. Она позволяет указывать для web.config файла вашего веб-приложения файл трансформации, который по определённому синтаксису (XDT) будет заменять, удалять или добавлять элементы файла web.config при развертывании. Синтаксис очень простой, потому сразу же захотелось использовать его, и не только для web.config, а и для log4net.config, unity.config и всех остальных config-файлов, которые лежат рядом. Более того, я сразу подумал о том, чтобы использовать эту фичу и для WinService приложения (консольное приложение в основе). И начал мечтать, как было бы хорошо, когда я бы настроил билд в CCNet и при развертывании у меня все настройки хорошо бы трансформировались по нужной конфигурации. Но оказалось не так все просто, данная фича по умолчанию работает только для web.config, но я решил не отчаиваться и найти решение, результатом которого стала небольшая программка Config Transformation Tool.

Сначала опишу задачу, которая передо мной стояла. Мы разрабатываем приложение, у которого есть несколько заказчиков, каждый из этих заказчиков требует или просит разные модули и настройки от нашего приложения. Поэтому при помощи конфигурационных файлов наше приложение может быть настроено до неузнаваемости. У нас есть CCNet build сервер, который помимо стандартных задач по построению приложения и прогону тестов, позволяет так же собирать специфичные для заказчика пакеты. До XDT трансформации я все реализовывал заменой в исходных config-файлах строк при помощи nant билда и его задачи replacestring с определенными параметрами. Скажу сразу, что было очень тяжело поддерживать все это при изменениях требований или добавлении настроек. Самый простой вариант был бы, наверное, просто наплодить по отдельному конфигурационному файлу для каждой инсталляции и положить в специальную папочку, а при развертывании просто заменять файл конфигурации на нужный, но тогда уже будут проблемы во время разработки, надо будет не забыть добавить нечто новое во все файлы конфигураций. В общем, после знакомства с Web.Config Transfomration я очень сильно на него рассчитывал.

Первое разочарование было, когда я узнал, что файл конфигурации работает только с web.config и нет возможности добавить такие файлы для других config файлов, подумал что не так уж и страшно, можно будет в итоге перенести всю конфигурацию в web.config, не очень удобно будет, но тоже решение. Второе разочарование было то, что для каждого развертывания мне придется делать отдельную настройку для проекта рядом с Debug и Release, а когда у нас будет 10 развертываний, делать и поддерживать 10 конфигураций, тоже как то не очень хотелось бы. Ну и третье разочарование было для меня, когда я узнал, что поддержка трансформаций есть только для веб-проектов, тут насколько я понял, разочаровывался не только я, и народ уже активно голосует за поддержку этой фичи и для app.config файлов на сайте connect.microsoft.com. Другие пользуются хаками, вроде этого Applying XDT magic to App.Config и получается это даже довольно успешно. Но все же – это было не то, что мне нужно.

Последняя статья меня подтолкнула на мысль, что, скорее всего, web.config Transformation – это просто задача для MsBuild, которую можно будет выполнить из командной строки. И я даже нашел заветную командную строку, но реально для несуществующих конфигураций (в проекте существовали стандартные конфигурации Debug и Release, а у меня был файл web.Company1.config, и конфигурацию я пытался вызвать соответственно Company1). Вызов задачи заканчивался непонятно и плачевно, видимо до конца я так и не отыскал все параметры, которые нужно выставить.

Я пошел дальше, открыл файл веб-проекта в редакторе и отыскал ссылку на импорт задач, в которой я и ожидал описание того как правильно вызвать задачу:

<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />

$(MSBuildExtensionsPath32) в моем случае был “C:\Program Files (x86)\MSBuild\”, там я нашел файл Microsoft.WebApplication.targets, а так же файл Microsoft.WebApplication.Build.Tasks.Dll, который привлек мое внимание больше. Вооружившись программой .Net Reflector, открыл этот файл и, проанализировав, нашел, что все задачи наследуются от типа Task, значит, по плану, мне просто нужно было бы найти нужную задачу.

.Net Reflector

Ее я нашел под названием TransformTask, сначала интуитивно подумал, что, наверное, это она и есть, а затем и под Reflector’ом посмотрев код в этом убедился на 100%. Решил попробовать встроить его в простенькое консольное приложение, которое позволяло бы мне выполнять эту задачу для любых файлов. Тут я замечу, что в случае использования MS Build, который в TFS, скорее всего, будет возможность вызвать эту задачу прямо из файла настройки билда, но наш случай – это CCNet. В будущем я, правда, планирую тесное знакомство с Ms Build, так как задумываемся о переходе с CCNet на MsBuild, но пока решил реализовать по-быстрому.

Итак, создал новый проект – консольное приложение и начал исследовать, как мне запустить данную задачу. Как оказалось все достаточно просто, нужно было реализовать два интерфейса. Первый: IBuildEngine, у него вызывается несколько методов для записи лога выполнения задачи, и пару свойств, смысла которых я не понял, и реализовал просто как затычки. Второй интерфейс - это ITaskItem. Кстати, хороший пример, как программировать не нужно. Подразумевает собой некоторую гибридную сущность, которая может нести в себе кучу информации, а может практически ничего не содержать в себе, как в моем случае: из кучи методов и свойств реализуем всего одно свойство ItemSpec, в котором будем хранить путь до файла. Вот в общем то и все, можно вызывать задачу:

TransformXml transformXml = new TransformXml
                                {                                    BuildEngine = new CurrentBuildEngine(),
                                    Destination = new TaskItem(destinationFilePath),
                                    Source = new TaskItem(sourceFilePath),
                                    Transform = new TaskItem(transformFile)
                                };
transformXml.Execute();

Дальше я причесал немного код, добавил тест, пример и выложил все на CodePlex, мало ли будет кому-то полезно. Думаю, вообще хорошо бы добавить еще поддержку параметров в файле трансформации, правда код MS править не хочется, который производит XDT трансформацию, а вот прекомпиляцию данного файла можно как-то сделать. Приблизительно идею описал в этой задаче Support parameters at Transform file.

Ну и на последок пример. Файл source.config, который хотим изменить:

<?xml version="1.0"?>
 <configuration>
     <custom>
        <groups>
            <group name="TestGroup1">
                <values>
                    <value key="Test1" value="True" />
                    <value key="Test2" value="600" />
                </values>
            </group>
             <group name="TestGroup2">
                <values>
                    <value key="Test3" value="True" />
                </values>
            </group>
         </groups>
    </custom>
</configuration>

Файл transform.config, при помощи которого будем менять исходный файл:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <custom>
        <groups>
            <group name="TestGroup1">
                <values>
                    <value key="Test2" value="601" xdt:Transform="Replace"  xdt:Locator="Match(key)" />
                </values>
            </group>
        </groups>
    </custom>
</configuration>

Запускаем приложение из командной строки

ctt.exe s:source.config t:transform.config d:destination.config

Config Transformation Tool In Action

Получаем результат destination.config:

<?xml version="1.0"?>
 <configuration>
     <custom>
        <groups>
            <group name="TestGroup1">
                <values>
                    <value key="Test1" value="True" />
                    <value key="Test2" value="601" />
                </values>
            </group>
             <group name="TestGroup2">
                <values>
                    <value key="Test3" value="True" />
                </values>
            </group>
         </groups>
    </custom>
</configuration>

Скачать исходный код или бинарник можно отсюда http://ctt.codeplex.com/.

Ну и как всегда, выслушаю все предложения и комментарии. ;)

Have a question? Want to follow up? Send a comment? Or just ask for help or consultation? Send me an email at public[at]denis[dot]gladkikh[one more dot]email.