Transform Your Sitecore Config Patches Using Build Targets

config patch Config Transform msbuild Sitecore

The concept of config patching is, hands down, what makes it super easy to bend Sitecore to your will. Sitecore itself uses a bunch of config patches to extend its own functionalities or add new ones. A fresh install will never completely work out-of-the-box because most features require some reconfiguration to fit the target environment and also the target server (e.g. Content Management, Content Delivery), which is best done using config patching at run-time so you wouldn’t have to manually update the default config files that came with the installer.

A Sitecore config patch that is environment-server-specific will definitely require different versions, therefore, config transformation is necessary to avoid manual modification. Though, by default, transformation is only supported for the Web.config file there are many tools available that will help you achieve this. However, most of them require that your config transforms do not go beyond the limits of your configurations. But in some cases there are config transforms that may be necessary for all configurations (base) or only for some server types (base.CM or base.CD). Thus, I’ll show you another approach using just good ol’ build targets.

This tutorial requires that you have a good understanding of build targets and the Visual Studio build process. If not, I would recommend doing some reading first, and here’s a couple of links to start off:

MSBuild Targets

How to: Extend the Visual Studio Build Process

The Config Patch

First, let’s create a config patch that will add a site configuration called ‘mysite’ to the sites collection. To ensure our config patch is evaluated after the default site config make sure to place it in App_Config\Include\Z_MySite\MySite.config.

<!-- MySite.config -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <sites>
      <site name="mysite"
        rootPath="/sitecore/content"
        startItem="/mysite"
        inherits="website"
        patch:before="*[@name='website']" />
    </sites>
  </sitecore>
</configuration>

The Config Transforms

Normally, the ASP.Net site is promoted thru a series of environments which are not limited to DEV, QA, and LIVE. On the other hand, a Sitecore site, for better performance and availability, should be deployed to at least two servers: Content Management (CM) and Content Delivery (CD). Hence, we should have configurations and config transforms matching the environment-server combination.

The first config transforms that we’re going to create will be for the CM and CD servers regardless of the environment. They will insert patches that will strictly set the database and content cacheability of the site depending on the target server type. FYI: I recommend patching the website config for such modifications if you have a multi-site setup, since the user-defined sites will most likely inherit from the website config.

<!-- MySite.base.CM.config -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">    
    <sites>
      <site name="website" xdt:Transform="Insert">
        <patch:attribute name="database" value="master" />
        <patch:attribute name="content" value="master" />
        <patch:attribute name="cacheHtml" value="false" />
      </site>
    </sites>
  </sitecore>
</configuration>

<!-- MySite.base.CD.config -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <sites>
      <site name="website" xdt:Transform="Insert">
        <patch:attribute name="database" value="web" />
        <patch:attribute name="content" value="web" />
        <patch:attribute name="cacheHtml" value="true" />
      </site>
    </sites>
  </sitecore>
</configuration>

The next config transforms will set the site’s hostname attribute that corresponds to the target environment and server type. Let’s use the DEV environment for this example:

<!-- MySite.DEV.CM.config -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <sites>
      <site name="mysite" hostname="dev-cm.mysite.com" 
        xdt:Transform="SetAttributes" 
        xdt:Locator="Match(name)" />
      </site>
    </sites>
  </sitecore>
</configuration>

<!-- MySite.DEV.CD.config -->
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <sites>
      <site name="mysite" hostname="dev.mysite.com" 
        xdt:Transform="SetAttributes" 
        xdt:Locator="Match(name)" />
      </site>
    </sites>
  </sitecore>
</configuration>

Adding the Config Transforms to the Project Tree

Once the transform files are created the .csproj file needs to be manually modified so that the transform files appear underneath the original config file in the project tree:

<ItemGroup> 
    <Content Include="App_Config\Include\Z_MySite\MySite.config">
      <SubType>Designer</SubType>
    </Content>
    <None Include="App_Config\Include\Z_MySite\MySite.base.CM.config">
      <DependentUpon>MySite.config</DependentUpon>
    </None>
    <None Include="App_Config\Include\Z_MySite\MySite.base.CD.config">
      <DependentUpon>MySite.config</DependentUpon>
    </None>
    <None Include="App_Config\Include\Z_MySite\MySite.DEV.CM.config">
      <DependentUpon>MySite.config</DependentUpon>
    </None>
    <None Include="App_Config\Include\Z_MySite\MySite.DEV.CD.config">
      <DependentUpon>MySite.config</DependentUpon>
    </None>
</ItemGroup>

The Build Targets

Now that the config transforms are in place we can proceed to transforming the original config patches using build targets. But before we create the ‘transform’ targets let’s first create a passive target to collect the config patch files that we’re going to transform:

<PropertyGroup>
  <TransformConfigDir>
    $(ProjectDir)obj\$(Configuration)\TransformConfig\
  </TransformConfigDir>
  <BaseServer Condition="$(Configuration.Contains('.CM'))">
    base.CM
  </BaseServer>
  <BaseServer Condition="$(Configuration.Contains('.CD'))">
    base.CD
  </BaseServer>
</PropertyGroup>

<Target Name="CollectProjectConfigFiles">
  <!-- Get all config files with transform file(s) -->
  <ItemGroup>
    <ConfigTransformFiles 
      Include="@(None)" 
      Condition="'%(None.Extension)' == '.config' 
                And '%(None.DependentUpon)' != ''" />
    <ConfigFiles 
      Include="@(ConfigTransformFiles->'%(RelativeDir)%(DependentUpon)')" />
  </ItemGroup>

  <RemoveDuplicates Inputs="@(ConfigFiles)">
    <Output
        TaskParameter="Filtered"
        ItemName="ConfigFilesToTransform"/>
  </RemoveDuplicates>

  <!-- Ensure all target directories exist -->
  <Delete 
    Files="$(TransformConfigDir)**\*" 
    Condition="Exists('$(TransformConfigDir)')" />
  <MakeDir 
    Directories="$(TransformConfigDir)%(ConfigFilesToTransform.RelativeDir)" 
    Condition="!Exists('$(TransformConfigDir)')" />

  <!--Copy the filtered config files into the target directory-->
  <Copy 
    SourceFiles="@(ConfigFilesToTransform)" 
    DestinationFiles="@(ConfigFilesToTransform->
                    '$(TransformConfigDir)\%(RelativeDir)
                    %(Filename)%(Extension)')" />

  <ItemGroup>
    <ProjectConfigFiles Include="$(TransformConfigDir)**\*" />
  </ItemGroup>
</Target>

The property ‘TransformConfigDir’ will be the directory that will hold the original config files while they are being transformed. The ‘BaseServer’ property will determine which ‘base.server’ transform file will be used depending on the selected build configuration.

The next step is to create the targets that will transform the config patch using the base, base.server, and configuration transform files respectively. Although we did not create an example for the base config transform (MySite.base.config), I’m throwing it in for good measure.

<Target Name="TransformConfigFilesUsingBase" 
  DependsOnTargets="CollectProjectConfigFiles">
  <TransformXml 
    Source="%(ProjectConfigFiles.Identity)"
    Transform="@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).base%(Extension)')"
    Destination="@(ProjectConfigFiles->'%(FullPath)')"
    Condition="Exists(@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).base%(Extension)'))"/>
</Target>

<Target Name="TransformConfigFilesUsingBaseServer" 
  DependsOnTargets="CollectProjectConfigFiles">
  <TransformXml 
    Source="%(ProjectConfigFiles.Identity)"
    Transform="@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).$(BaseServer)%(Extension)')"
    Destination="@(ProjectConfigFiles->'%(FullPath)')"
    Condition="Exists(@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).$(BaseServer)%(Extension)'))"/>
</Target>

<Target Name="TransformConfigFilesUsingConfiguration"
  DependsOnTargets="CollectProjectConfigFiles">
  <TransformXml 
    Source="%(ProjectConfigFiles.Identity)"
    Transform="@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).$(Configuration)%(Extension)')"
    Destination="@(ProjectConfigFiles->'%(FullPath)')"
    Condition="Exists(@(ProjectConfigFiles->
              '$(ProjectDir)%(RecursiveDir)
              %(Filename).$(Configuration)%(Extension)'))"/>
</Target>

<Target Name="TransformConfigFiles">
  <CallTarget Targets="TransformConfigFilesUsingBase;
              TransformConfigFilesUsingBaseServer;
              TransformConfigFilesUsingConfiguration" />
</Target>

Triggering the Build Targets

You must have noticed that the ‘TransformConfigFiles’ will not be triggered since no other target will execute it. In order to do so let’s create a new target that will depend upon it, and hook that new target to the PipelineCopyAllFilesToOneFolderForMsdeploy target to ensure our transformed config files will be copied to the package temp folder before MS deploy:

<Target Name="CopyTransformedConfigFiles"
  DependsOnTargets="TransformConfigFiles" 
  BeforeTargets="PipelineCopyAllFilesToOneFolderForMsdeploy">
  <ItemGroup>
    <_ConfigFiles Include="$(TransformConfigDir)**\*" />
  </ItemGroup>

  <Copy SourceFiles="@(_ConfigFiles)" 
        DestinationFiles="@(_ConfigFiles->
                          '$(_PackageTempDir)\%(RecursiveDir)
                          %(Filename)%(Extension)')" />
</Target>

I highly recommend having all these properties and targets in one .targets file (e.g. ConfigTransform.targets), and import this file into the .csproj file containing your website project. Assuming the .targets file is in the solution’s \tools\buildtargets subdirectory here’s how you should declare the import:

<Project ToolsVersion="12.0" DefaultTargets="Build"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <Import Project="$(SolutionDir)tools\BuildTargets\ConfigTransform.targets" />
</Project>

Now publish the ASP.Net website to an IIS server or create a Web Deploy package and you’ll see that our MySite.config file has been transformed to whichever environment configuration you’ve selected beforehand.

What about the Web.config?

Take note that this approach will transform any .config file in your project that has corresponding transform files, including the Web.config file, which means we can now create base and base.server transform files for it as well. And since we don’t have any use for the default web.config transform target,  TransformWebConfigCore, we can now disable it using another target that will be called before the default one:

<Target Name="DisableTransformWebConfigCore"
  BeforeTargets="TransformWebConfigCore">
  <ItemGroup>
    <WebConfigsToTransform Remove="*.*" />
  </ItemGroup>
</Target>

I’ve written another blog on how you can update your local IIS web server upon building your ASP.Net website, and the approach that I showed you will come in handy when you want to sync your Sitecore solution with your local Sitecore website.

Happy programming!

3 Thoughts to Transform Your Sitecore Config Patches Using Build Targets

Comments are closed.

  • gregory mertens
    gregory mertens

    Question. When you deploy your solution, are you doing a seperate publish for each environment (staging, UAT, Prod)?

    Our current approach has been to publish it just once. Any config files that varied between environments are left out of source control (not good). But publishing multiple times seems like extra work and seems like at least a small opportunity for discrepancies between environments.

    Thanks,
    Gregory

    23.03.2018 17:03
  • JC
    JC

    Thank you! I was just starting to dig into the guts of MSBUILD and web deploy when I came across this post. This is exactly the problem I was trying to solve.

    20.04.2017 19:20
    • Jeff Sulit
      Jeff Sulit

      You’re welcome, Josh! Glad to be of help.

      20.04.2017 19:31