Introduction

Often we will want to XML-preprocess our own files, to update them with environment specific settings. This can be done easily using the BizTalk Deployment Framework by using the FilesToXmlPreprocess ItemGroup. However, if the file's content contains any text which is the same as BTDF's placeholder syntax (${sometext}), this gives issues, because the XML-Preprocessor will also try to update this. For example, let's say you want to add an NLog.config file like the following.

<?xml version="1.0" encoding="utf-8" ?>
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Warn"
      internalLogFile="E:\Logging\NLog\nlog-internal.log" >
 
  <targets async="true">
    <target name="pipelinelogfile" xsi:type="File" fileName="E:\Logging\NLog\pipeline_file.txt" layout="${message}" />
    <target name="orchestrationlogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="${message}" />
    <target name="generallogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="${message}" />
  </targets>
 
  <rules>
    <logger name="Contoso.Common.Logging.BizTalkPipelineLogger" minlevel="Error" writeTo="pipelinelogfile" />
    <logger name="Contoso.Common.Logging.GeneralLogger" minlevel="Warn" writeTo="generallogfile" />
    <logger name="Contoso.Common.Logging.BizTalkOrchestrationLogger" minlevel="Error" writeTo="orchestrationlogfile" />
  </rules>
</nlog>

Here we want to update the E:\Logging\NLog placeholder with an environment specific path, however, we want ${message} to remain untouched, as this is an NLog specific layout. Luckily BTDF can easily be extended using MSBuild, which we will use here to update our contents using a regular expression.

Prepare your file

First, we will want to prepare our file by replacing ${message} with another placeholder, in this case, we will use _REPLACE_.

<?xml version="1.0" encoding="utf-8" ?>
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Warn"
      internalLogFile="E:\Logging\NLog\nlog-internal.log" >
 
  <targets async="true">
    <target name="pipelinelogfile" xsi:type="File" fileName="E:\Logging\NLog\pipeline_file.txt" layout="_REPLACE_" />
    <target name="orchestrationlogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="_REPLACE_" />
    <target name="generallogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="_REPLACE_" />
  </targets>
 
  <rules>
    <logger name="Contoso.Common.Logging.BizTalkPipelineLogger" minlevel="Error" writeTo="pipelinelogfile" />
    <logger name="Contoso.Common.Logging.GeneralLogger" minlevel="Warn" writeTo="generallogfile" />
    <logger name="Contoso.Common.Logging.BizTalkOrchestrationLogger" minlevel="Error" writeTo="orchestrationlogfile" />
  </rules>
</nlog>

Preprocess XML File

Next, we will update our btdfproj file to preprocess the config file. This will replace the logdirectory with the environment specific variable.

<!-- Preprocess XML Files -->
<ItemGroup>
    <FilesToXmlPreprocess Include="NLog.Master.config">
      <LocationPath>..\</LocationPath>
      <OutputFilename>NLogBeforeReplace.config</OutputFilename>
    </FilesToXmlPreprocess>
</ItemGroup>

Update custom placeholder

We will use MSBuild to replace our custom _REPLACE_ placeholder. For this, we will call some C# code which applies a regular expression.

<!-- Find and replace text in textfiles -->
<UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <InputFilename ParameterType="System.String" Required="true" />
      <OutputFilename ParameterType="System.String" Required="true" />
      <MatchExpression ParameterType="System.String" Required="true" />
      <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
              File.WriteAllText(
                  OutputFilename,
                  Regex.Replace(File.ReadAllText(InputFilename), MatchExpression, ReplacementText)
                  );
            ]]>
      </Code>
    </Task>
</UsingTask>

Now we can call this task from a new target. As we want this to happen after the file has been preprocessed, we will use the AfterTargets attribute.

<!-- Add the ${message} placeholder to our NLog config. This can not be placed in here initially, as the XML Preprocessor will try to replace it. -->
<Target Name="UpdateNLogConfig" AfterTargets="PreprocessFiles">
    <ReplaceFileText InputFilename="..\NLogBeforeReplace.config" OutputFilename="..\NLog.config" MatchExpression="_TOREPLACE_" ReplacementText="${message}" />
</Target>

Copy final file

Our config file is now complete with the correct environment settings and our NLog layout set correctly.

<?xml version="1.0" encoding="utf-8" ?>
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Warn"
      internalLogFile="E:\Logging\NLog\nlog-internal.log" >
 
  <targets async="true">
    <target name="pipelinelogfile" xsi:type="File" fileName="E:\Logging\NLog\pipeline_file.txt" layout="${message}" />
    <target name="orchestrationlogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="${message}" />
    <target name="generallogfile" xsi:type="File" fileName="E:\Logging\NLog\orchestration_file.txt" layout="${message}" />
  </targets>
 
  <rules>
    <logger name="Contoso.Common.Logging.BizTalkPipelineLogger" minlevel="Error" writeTo="pipelinelogfile" />
    <logger name="Contoso.Common.Logging.GeneralLogger" minlevel="Warn" writeTo="generallogfile" />
    <logger name="Contoso.Common.Logging.BizTalkOrchestrationLogger" minlevel="Error" writeTo="orchestrationlogfile" />
  </rules>
</nlog>

Now we just have to make sure the file is placed in the correct locations. In this example we will add it to two web services (which will also be deployed using BTDF), so any files received in them can be logged. Once again we will use the AfterTargets attribute to make sure this happens at the correct time, and use MSBuild to copy the file.

<!-- Place NLog.config in webservices directories -->
<Target Name="CopyPreprocessedNLogConfig" AfterTargets="UpdateNLogConfig">
    <ItemGroup>
        <NLogConfigToCopy Include="..\NLog.config" />
    </ItemGroup>
    <Copy DestinationFolder="..\IIS\MyFirstWebservice" SourceFiles="@(NLogConfigToCopy)" />
    <Copy DestinationFolder="..\IIS\MySecondWebservice" SourceFiles="@(NLogConfigToCopy)" />
</Target>

Your btdfproj file should now look something like this.

<?xml version="1.0" encoding="utf-8"?>
<!--
  Deployment Framework for BizTalk
  Copyright (C) 2004-2012 Thomas F. Abraham and Scott Colestock
-->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Installer" ToolsVersion="4.0">
  <PropertyGroup>
    <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
    <Platform Condition="'$(Platform)' == ''">x86</Platform>
    <SchemaVersion>1.0</SchemaVersion>
    <ProjectName>Contoso.MyProject</ProjectName>
    <ProjectVersion>1.0</ProjectVersion>
    <IncludeSSO>False</IncludeSSO>
    <IncludeTransforms>True</IncludeTransforms>
    <IncludeOrchestrations>True</IncludeOrchestrations>
    <IncludeVirtualDirectories>True</IncludeVirtualDirectories>
    <UsingMasterBindings>True</UsingMasterBindings>
    <RequireXmlPreprocessDirectives>False</RequireXmlPreprocessDirectives>
    <ApplyXmlEscape>True</ApplyXmlEscape>
    <SkipIISReset>False</SkipIISReset>
  </PropertyGroup>
  <PropertyGroup>
    <ProductVersion>1.0.0</ProductVersion>
    <ProductId>b82923aa-5b64-4f21-8d62-e2e446398508</ProductId>
    <ProductName>Contoso.MyProject for BizTalk</ProductName>
    <Manufacturer>Deployment Framework User</Manufacturer>
    <PackageDescription>Contoso.MyProject</PackageDescription>
    <PackageComments>Contoso.MyProject</PackageComments>
    <ProductUpgradeCode>dc669b36-5031-42df-9c81-034d94f9bbd7</ProductUpgradeCode>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
    <DeploymentFrameworkTargetsPath>$(MSBuildExtensionsPath)\DeploymentFrameworkForBizTalk\5.0\</DeploymentFrameworkTargetsPath>
    <OutputPath Condition="'$(TeamBuildOutDir)' == ''">bin\Debug\</OutputPath>
    <OutputPath Condition="'$(TeamBuildOutDir)' != ''">$(TeamBuildOutDir)</OutputPath>
    <DeployPDBsToGac>false</DeployPDBsToGac>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <DeploymentFrameworkTargetsPath>$(MSBuildExtensionsPath)\DeploymentFrameworkForBizTalk\5.0\</DeploymentFrameworkTargetsPath>
    <OutputPath Condition="'$(TeamBuildOutDir)' == ''">bin\Release\</OutputPath>
    <OutputPath Condition="'$(TeamBuildOutDir)' != ''">$(TeamBuildOutDir)</OutputPath>
    <DeployPDBsToGac>false</DeployPDBsToGac>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)' == 'Server'">
    <DeploymentFrameworkTargetsPath>Framework\</DeploymentFrameworkTargetsPath>
    <!-- Get our PDBs into the GAC so we get file/line number information in stack traces. -->
    <DeployPDBsToGac>true</DeployPDBsToGac>
  </PropertyGroup>
 
  <ItemGroup>
    <VDirList Include="*">
      <Vdir>MyFirstWebservice</Vdir>
      <AppPool>BizTalkWCF</AppPool>
      <Physdir>..\IIS\MyFirstWebservice</Physdir>
      <AppPoolNetVersion>v4.0</AppPoolNetVersion>
    </VDirList>
    <VDirList Include="*">
      <Vdir>MySecondWebservice</Vdir>
      <AppPool>BizTalkWCF</AppPool>
      <Physdir>..\IIS\MySecondWebservice</Physdir>
      <AppPoolNetVersion>v4.0</AppPoolNetVersion>
    </VDirList>
  </ItemGroup>
   
  <ItemGroup>
    <AppsToReference Include="Contoso.CoreComponents;Microsoft.Practices.ESB" />
  </ItemGroup>
 
  <ItemGroup>
    <PropsFromEnvSettings Include="SsoAppUserGroup;SsoAppAdminGroup;VDIR_UserName;VDIR_UserPass" />
  </ItemGroup>
 
  <!-- Include additional files in MSI -->
  <ItemGroup>
    <AdditionalFiles Include="NLog.Master.config">
      <LocationPath>..\</LocationPath>
    </AdditionalFiles>
  </ItemGroup>
 
  <!-- Preprocess XML Files -->
  <ItemGroup>
    <FilesToXmlPreprocess Include="NLog.Master.config">
      <LocationPath>..\</LocationPath>
      <OutputFilename>NLogBeforeReplace.config</OutputFilename>
    </FilesToXmlPreprocess>
  </ItemGroup>
 
  <!-- Add the ${message} placeholder to our NLog config. This can not be placed in here initially, as the XML Preprocessor will try to replace it. -->
  <Target Name="UpdateNLogConfig" AfterTargets="PreprocessFiles">
    <ReplaceFileText InputFilename="..\NLogBeforeReplace.config" OutputFilename="..\NLog.config" MatchExpression="_TOREPLACE_" ReplacementText="${message}" />
  </Target>
 
  <!-- Place NLog.config in webservices directories -->
  <Target Name="CopyPreprocessedNLogConfig" AfterTargets="UpdateNLogConfig">
    <ItemGroup>
      <NLogConfigToCopy Include="..\NLog.config" />
    </ItemGroup>
    <Copy DestinationFolder="..\IIS\MyFirstWebservice" SourceFiles="@(NLogConfigToCopy)" />
    <Copy DestinationFolder="..\IIS\MySecondWebservice" SourceFiles="@(NLogConfigToCopy)" />
  </Target>
 
  <!-- Find and replace text in textfiles -->
  <UsingTask TaskName="ReplaceFileText" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <InputFilename ParameterType="System.String" Required="true" />
      <OutputFilename ParameterType="System.String" Required="true" />
      <MatchExpression ParameterType="System.String" Required="true" />
      <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
              File.WriteAllText(
                  OutputFilename,
                  Regex.Replace(File.ReadAllText(InputFilename), MatchExpression, ReplacementText)
                  );
            ]]>
      </Code>
    </Task>
  </UsingTask>
   
  <Import Project="$(DeploymentFrameworkTargetsPath)BizTalkDeploymentFramework.targets" />
  <Target Name="CustomRedist">
  </Target>
</Project>

See Also

Another important place to find an extensive amount of BizTalk related articles is the TechNet Wiki itself. The best entry point is  BizTalk Server Resources on the TechNet Wiki.