Скачать исходный код |
Мы рассмотрели основы трансформации Web.config. Теперь стоит углубится в тему сборки проекта и рассмотреть детально, когда и как делается трансформация файлов конфигурации.
Я считаю, что без детального понимания процесса сборки и трансформации, мы будем использовать эти инструменты не достаточно эффективно и можем не видеть причин возникающих ошибок.
Как происходит сборка проекта
Файл проекта представляет XML-код для утилиты MSBuild. Предназначение у этой утилиты схоже с NAnt и Psake, но в отличие от них MSBuild используется в по-умолчанию в Visual Studio. Когда в VS мы нажимаем Build, фактически в MSBuild запускается наш файл проекта.
Для лучшего понимания стоит изучить основы работы с MSBuild в книге Inside the Microsoft® Build Engine: Using MSBuild and Team Foundation Build.
Из чего состоит файл проекта в Visual Studio
Рассмотрим основные части файла проекта. Из кода убраны повторяющиеся и неважные части.
MvcApplication.csproj:
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> <Reference Include="Microsoft.CSharp" /> <Reference Include="System" /> <Reference Include="System.Data" /> </ItemGroup> <ItemGroup> <Compile Include="App_Start\AuthConfig.cs" /> <Compile Include="App_Start\BundleConfig.cs" /> </ItemGroup> <ItemGroup> <Content Include="Content\themes\base\images\ui-bg_flat_0_aaaaaa_40x100.png" /> <Content Include="Content\themes\base\images\ui-bg_flat_75_ffffff_40x100.png" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" /> <Target Name="MvcBuildViews" AfterTargets="AfterBuild" Condition="'$(MvcBuildViews)'=='true'"> <AspNetCompiler VirtualPath="temp" PhysicalPath="$(WebProjectOutputDir)" /> </Target> <ProjectExtensions> </ProjectExtensions> <Target Name="AfterBuild"> <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" /> </Target> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> </Project>
В начале XML-кода мы видим, что первым будет запущен DefaultTargets, который называется Build:
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Этот target определен в Microsoft.Common.targets, как:
<PropertyGroup> <BuildDependsOn> BeforeBuild; CoreBuild; AfterBuild </BuildDependsOn> </PropertyGroup> <Target Name="Build" Condition=" '$(_InvalidConfigurationWarning)' != 'true' " DependsOnTargets="$(BuildDependsOn)" Returns="$(TargetPath)" />
В свойстве BuildDependsOn видно основную последовательность сборки проекта. BeforeBuild и AfterBuild мы будем определять сами, как, например, в прошлый раз в AfterBuild мы добавляли TransformXml. Стоит посмотреть на последовательность выполнения CoreBuild, которые определены в том же файле:
<PropertyGroup> <CoreBuildDependsOn> BuildOnlySettings; PrepareForBuild; PreBuildEvent; ResolveReferences; PrepareResources; ResolveKeySource; Compile; ExportWindowsMDFile; UnmanagedUnregistration; GenerateSerializationAssemblies; CreateSatelliteAssemblies; GenerateManifests; GetTargetPath; PrepareForRun; UnmanagedRegistration; IncrementalClean; PostBuildEvent </CoreBuildDependsOn> </PropertyGroup> <Target Name="CoreBuild" DependsOnTargets="$(CoreBuildDependsOn)">
Здесь мы видим более детальную последовательность вызовов всех этапов сборки нашего проекта. Каждый из этих этапов можно посмотреть в файлах *.targets.
Дальше в файле проекта идет определение настроек для каждой конфигурации сборки. В нашем случае есть только две конфигурации:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize>
После этого определяются ItemGroup'ы для ссылок на сторонние сборки, файлы для компиляции и контент проекта.
<ItemGroup> <Compile Include="App_Start\AuthConfig.cs" /> <Compile Include="App_Start\BundleConfig.cs" />
После этого идет основная часть, в которой содержатся все определения для нашего проекта. Подключаются Microsoft.CSharp.targets (внутри него подключается знакомый нам Microsoft.Common.targets) и Microsoft.WebApplication.targets:
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
MSBuild, как основной инструмент сборки
Мы увидели, что наши проекты это просто код, который выполняется утилитой MSBuild. Любой Target можно переопределить или дополнить своим кодом. В основном изменениям подвергаются BeforeBuild и AfterBuild.
Что такое Build, Rebuild, Clean?
Мы все пользуемся этими тремя функциями в Visual Studio:
Что значит Build мы только что подробно рассмотрели. Теперь остановимся на Rebuild и Clean. Как можно было догадаться это обычные target'ы, которые определены в Microsoft.Common.targets. Когда мы выбираем в меню команду Rebuild, то MSBuild запускает не DefaultTargets, а Rebuild. Аналогично происходит с Clean.
Clean определен следующим образом:
<PropertyGroup> <CleanDependsOn> BeforeClean; UnmanagedUnregistration; CoreClean; CleanReferencedProjects; CleanPublishFolder; AfterClean </CleanDependsOn> </PropertyGroup> <Target Name="Clean" Condition=" '$(_InvalidConfigurationWarning)' != 'true' " DependsOnTargets="$(CleanDependsOn)" />
Если посмотреть по реализации каждого из target'ов, то мы увидим, что отчищаются все промежуточные файлы и результаты сборок проекта.
Rebuild определен в том же файле:
<PropertyGroup> <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' != ''">$(MSBuildProjectDefaultTargets)</_ProjectDefaultTargets> <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' == ''">Build</_ProjectDefaultTargets> <RebuildDependsOn> BeforeRebuild; Clean; $(_ProjectDefaultTargets); AfterRebuild; </RebuildDependsOn> </PropertyGroup> <Target Name="Rebuild" Condition=" '$(_InvalidConfigurationWarning)' != 'true' " DependsOnTargets="$(RebuildDependsOn)" Returns="$(TargetPath)"/>
Фактически Rebuild является комбинацией из Clean и Build.
Основные директории *.targets
Вы можете сами увидеть и изучить все основные target'ы, которые уже определены и реализованы в папках:
- %programfiles%\MSBuild
- %windir%\Microsoft.NET\Framework\версия
Процесс трансформации Web.config
В примере с трансформацией Web.config мы добавили следующий код в AfterBuild:
MvcApplication.csproj:
... <Target Name="AfterBuild"> <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" /> </Target> ...
Выше мы рассмотрели, что AfterBuild вызывается в самом конце сборки проекта.
Задача TransformXml
Рассмотрим где и как определена задача TransformXml. Мы импортировали файл Microsoft.WebApplication.targets, который в свою очередь импортирует Microsoft.Web.Publishing.targets. В последнем и определена наша задача:
<UsingTask TaskName="TransformXml" AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"/>
Посмотрим внутренности сборки Microsoft.Web.Publishing.Tasks.dll через dotPeek. У нас есть стандартный класс, который унаследован от Task. В методе Execute видим:
namespace Microsoft.Web.Publishing.Tasks { public class TransformXml : Task { // ... public override bool Execute() { // ... document = this.OpenSourceFile(this.Source); xmlTransformation = this.OpenTransformFile(this.Transform, logger); flag = xmlTransformation.Apply((XmlDocument) document); // ... } }
Идем дальше и смотрим реализацию метода Apply и доходим до TransformLoop и дальше корни уходят в сборку Microsoft.Web.XmlTransform.dll:
namespace Microsoft.Web.XmlTransform { public class XmlTransformation : IServiceProvider, IDisposable { private void TransformLoop(XmlNodeContext parentContext) { foreach (XmlNode xmlNode in parentContext.Node.ChildNodes) { XmlElement element = xmlNode as XmlElement; if (element != null) { XmlElementContext elementContext = this.CreateElementContext(parentContext as XmlElementContext, element); // ... this.HandleElement(elementContext); // ... } } } } }
Получается, что команда TransformXml по определенным XPath выполняет нужные нам трансформации.
Вызов TransformXml в файле проекта
Мы видели, как трансформация работает внутри, теперь будет довольно легко разобраться с самим вызовом:
MvcApplication.csproj:
<Target Name="AfterBuild"> <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" /> </Target>
Исходный файл Source, который мы будем изменять, выставлен в значение Web.config. MSBuild будет ссылаться на директорию проекта, когда начнет искать этот файл.
Описание трансформации Transform выставлено в Web.$(Configuration).config. Переменная $(Configuration) выставляется в зависимости от типа конфигурации, выбранной в Visual Studio, либо в Microsoft.Common.targets:
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
Получается, что если в VS мы выбрали конфигурацию Debug для сборки проекта, то значение Transform будет равно Web.Debug.config.
Преобразованный файл мы записываем в Destination, в нашем случае это Web.config.
Создание своих Task для MSBuild
Вы можете создавать свои задачи по примеру TransformXml. Для этого достаточно создать класс, унаследованные от Task и подключить задачу через UsingTask.
Для многих задач уже есть готовые наборы Task'ов. Кроме них, я рекомендую использовать наборы от сообщества разработчиков с открытым исходным кодом:
- http://msbuildextensionpack.codeplex.com - The MSBuild Extension Pack provides a collection of over 480 MSBuild Tasks, MSBuild Loggers and MSBuild TaskFactories
- https://github.com/loresoft/msbuildtasks - The MSBuild Community Tasks Project is an open source project for MSBuild tasks
Всё в целом
Мы подробно рассмотрели процесс сборки проекта, чтобы он не был черным ящиком с магической кнопкой Build на панеле задач. Исходя из процесса, рассмотрели когда и как срабатывает функция TransformXml, которая трансформирует Web.config.
Как можно увидеть предложенный способ трансформации является одним из возможных, т.к процесс сборки построен очень гибко и доступен для расширения.
Статья из серии Continuous Integration: Работа с Config-файлами:
Спасибо. Оказывается всё просто :-)
ОтветитьУдалить