The ability to build and deploy to multiple environments is a true enterprise application development requirement. In many cases, the out-of-the-box build and deploy processes usually lack sufficient support. If you maintain multiple environments then you will probably agree with this observation. In the majority of my past projects, we maintained one or more environments for development, testing, staging, integration, QA, Production, training and others. The environments will require unique values including database connections, paths, email addresses and IP addresses. How do we manage these unique property values when attempting to automate the deployment process?

I have participated in several projects utilizing open source scripting tools. This is true for Java and .NET projects, which are faced with the same build challenges. The most popular tool is Ant or NAnt, which both have a loyal following that continue to contribute to the projects.

The main objective is a simple and intuitive build process, so the maintenance requires minimal skills. I rarely build and deploy from the IDE (except local), so the process should allow external execution with integration to the source control repository of choice. The scripting approach is also attractive for Continuous Integration, which provides significant benefits to any enterprise system.

We will select NAnt for this article, but the same techniques can easily be applied to the Java flavor (Ant) with a few adjustments. NAnt/Ant is an XML-based scripting tool with a high-level project element containing a series of callable targets. A target contains one or more tasks. You can download NAnt and unzip the binary distribution. I also recommend the NAntContrib project, which is a collection of tasks and functions that extend the capabilities of NAnt.

Once you complete the setup, the following is a simple NAnt script to build a solution. It is just the basic script to perform a simple build.

01
<?xml version="1.0" encoding="utf-8" ?>
02
<project name="MySolution" default="build">
03
  <!-- Set project properties-->
04
  <property name="name" value="MyNAntBuildSolution"/>
05
  <property name="build.source" value="${name}"/>
06
  <property name="build.out" value="${name}\bin\Release"/>
07
  <property name="build.target" value="release" />
08
 
09
<target name="init" description="create target folder" >
10
 <mkdir dir="${build.target}" />
11
</target>
12
 
13
<target name="clean" description="delete the target folder">
14
 <delete dir="${build.target}" failonerror="false"/>
15
  <delete dir="${build.out}" failonerror="false"/>
16
</target>
17
 
18
<target name="build" depends="clean,init" description="Build">
19
 <call target="compile"/>
20
 <copy todir="${build.target}">
21
 <fileset basedir="${build.out}">
22
      <include name="*.config" />
23
      <include name="*.exe" />
24
    </fileset>
25
 </copy>
26
</target>
27
 
28
<target name="compile" description="compile using release configuration">
29
 <exec program="${environment::get-variable('SYSTEMROOT')}\Microsoft.Net\Framework\v4.0.30319\msbuild.exe">
30
 <arg value="${name}.sln" />
31
 <arg value="/p:configuration=release"/>
32
 </exec>
33
</target>
34
</project>

In the above script, we define the solution (name), build.source, build.out and build.target properties. We created the clean and init targets to simply delete and create folders. The default build target performs our build process by calling the compile target and performing several tasks, which also depends on the init and clean targets. The compile target executes the MSBuild using the release configuration. The remaining build target tasks copy the MSBuild results to the build.target folder, which is ready for deployment.

So…this was simple enough to create a basic build script. The next challenge is supporting the unique target environments, so we can inject or replace property values during the build to customize the configuration. First, we can create a configuration file with placeholders or tokens representing a value that will change based on the target environment. As we stated earlier, we would like this process to be easy to maintain. So…we will create the dev.config XML file with easy to understand property elements with name-value pair attributes.

1
<?xml version="1.0" encoding="utf-8" ?>
2
<project>
3
  <property name="environment.name" value="Development"/>
4
</project> 

The above configuration file should be created for each target environment and contain all the properties with unique values per environment. Next, edit your application configuration file and replace the static value with a placeholder or token. The placeholder instructs the build script to replace the current value with the appropriate value from the configuration file based on the name. For example: @environment.name@ will be replaced Development using the above example. The following is the configuration file with the placeholders.

1
<?xml version="1.0" encoding="utf-8" ?>
2
<configuration>
3
  <appSettings>
4
    <add key="environment" value="@environment.name@"/>
5
  </appSettings>
6
</configuration>

Next, create a process to transform the placeholders into the values. NAnt offers a style task, which will provide the transformation services that are required. The script will create a dynamic NAnt target to replace tokens or placeholders with the appropriate values. The following is the XSLT to generate the dynamic NAnt target.

01
<?xml version="1.0" encoding="UTF-8"?>
02
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
03
<xsl:output method="xml" indent="yes"/>
04
<xsl:template match="/">
05
<xsl:apply-templates/>
06
</xsl:template>
07
<xsl:template match="project">
08
<xsl:element name="project">
09
<xsl:element name="target">
10
   <xsl:attribute name="name">token-replacement</xsl:attribute>
11
   <xsl:element name="copy">
12
      <xsl:attribute name="todir">${token-replacement.todir}</xsl:attribute>
13
      <xsl:attribute name="overwrite">true</xsl:attribute>
14
      <xsl:element name="fileset">
15
         <xsl:attribute name="basedir">${token-replacement.basedir}</xsl:attribute>
16
         <xsl:attribute name="defaultexcludes">true</xsl:attribute>
17
         <xsl:element name="include">
18
            <xsl:attribute name="name">${token-replacement.include}</xsl:attribute>
19
         </xsl:element>
20
      </xsl:element>
21
      <xsl:element name="filterchain">
22
         <xsl:element name="replacetokens">
23
            <xsl:for-each select="//property">
24
               <xsl:element name="token">
25
                  <xsl:attribute name="key">
26
                     <xsl:value-of select="@name"/>
27
                  </xsl:attribute> 
28
                  <xsl:attribute name="value">
29
                     <xsl:value-of select="@value"/>
30
                  </xsl:attribute> 
31
               </xsl:element>
32
            </xsl:for-each>
33
         </xsl:element>
34
   </xsl:element>
35
</xsl:element>
36
</xsl:element>
37
</xsl:element>
38
</xsl:template>
39
</xsl:stylesheet>

Why create a dynamic target? We would like to decouple the configuration information from the build script. In this design, the build script remains unchanged regardless of modifications to the configuration information. You can also maintain separate environment-specific configuration information. So…let’s look at the NAnt build script task to generate the dynamic target.

1
 <style in="${env}.config" style="token-replacement.xslt" out="token-replacement.frag" />

Once you generate the target fragment, you must instruct NAnt to load the target dynamically. The following task will load the generated target fragment into the script. You can now call this target.

1
<include buildfile="token-replacement.frag" failonerror="false"/>

The available source code download generates the following token-replacement.frag file, which is loaded using the include task above and created by the previous style task. The target takes advantage of the replacetokens task while performing the copy process. Since this target is part of the build, you have access to all build properties.

01
<?xml version="1.0" encoding="utf-8"?>
02
<project>
03
  <target name="token-replacement">
04
    <copy todir="${token-replacement.todir}" overwrite="true">
05
      <fileset basedir="${token-replacement.basedir}" defaultexcludes="true">
06
        <include name="${token-replacement.include}" />
07
      </fileset>
08
      <filterchain>
09
        <replacetokens>
10
          <token key="environment.name" value="Development" />
11
        </replacetokens>
12
      </filterchain>
13
    </copy>
14
  </target>
15
</project>

The following is the complete NAnt build script including the transform-config target, which we can also call via Visual Studio as a Post-build event process to perform the configuration transformation for local debug modes.

01
<?xml version="1.0" encoding="utf-8" ?>
02
<project name="MyNAntBuildSolution" default="build">
03
  <!-- Set common project properties-->
04
  <property name="name" value="${project::get-name()}"/>
05
  <property name="build.source" value="${name}"/>
06
  <property name="build.out" value="${name}\bin\Release"/>
07
  <property name="build.target" value="release" />
08
  <property name="app.config" value="${name}.exe.config"/>
09
 
10
  <!-- Create a dynamic NAnt target to replace tokens in
11
       configuration file-->
12
  <echo message="Creating token-replacement target..."/>
13
  <delete file="token-replacement.frag" 
14
          failonerror="false"/>
15
  <style in="${env}.config" 
16
         style="token-replacement.xslt" 
17
         out="token-replacement.frag" />
18
  <echo message="Loading token-replacement target..."/>
19
  <include buildfile="token-replacement.frag" 
20
           failonerror="false"/>
21
 
22
  <!--- Intialize to create target folders -->
23
  <target name="init" description="create target folder" >
24
    <mkdir dir="${build.target}" />
25
  </target>
26
  <!-- Clean to remove old target folders -->
27
  <target name="clean" description="delete the target folder">
28
    <delete dir="${build.target}"
29
            failonerror="false"/>
30
    <delete dir="${build.out}"
31
            failonerror="false"/>
32
  </target>
33
  <!-- Build to compile and assemble the deployment artifacts -->
34
  <target name="build" depends="clean,init" description="Build">
35
    <call target="compile"/>
36
    <call target="transform-config"/>
37
    <copy todir="${build.target}">
38
      <fileset basedir="${build.out}">
39
        <include name="*.config" />
40
        <include name="*.exe" />
41
      </fileset>
42
    </copy>
43
  </target>
44
  <!-- Compile to call MSBuild -->
45
  <target name="compile" description="compile using release configuration">
46
    <exec program="${environment::get-variable('SYSTEMROOT')}\Microsoft.Net\Framework\v4.0.30319\msbuild.exe">
47
      <arg value="${name}.sln" />
48
      <arg value="/p:configuration=release"/>
49
    </exec>
50
  </target>
51
  <!-- Transform Config to convert the application
52
        config file using transformation -->
53
  <target name="transform-config">
54
    <property name="token-replacement.basedir"
55
              value="${build.out}"/>
56
    <property name="token-replacement.todir"
57
              value="${directory::get-current-directory()}"/>
58
    <property name="token-replacement.include"
59
              value="${app.config}"/>
60
    <call target="token-replacement"/>
61
    <!-- Replace build.out file with transform file -->
62
    <move file="${app.config}"
63
          tofile="${build.out}\${app.config}"
64
          overwrite="true"/>
65
  </target>
66
</project>

What’s next? Well, the addition of a target to handle the source control checkout/Get Latest prior to executing the build target would be very useful. NAnt and NAntContrib contain tasks supporting many popular source control products, so adding this feature requires just a little configuration for your repository. You can also include targets to run your regression tests (e.g. NUnit) and produce informative reports. Finally, the deployment task would be required to move the release folder to the target server. Again, several tasks are available to add this feature to your build script.

You can download the source code (see comments for additional information), which contains a build script for a simple console application. You can run the build script from the command prompt or download the NAnt Visual Studio plug-in. The following is the command-line to run the script, which requires the environment configuration file name as a parameter.

NAnt.exe -D:env=dev

In conclusion, using NAnt as your external build tool offers many out-of-the-box features. I am sure you will find answers to many of your challenging build and deploy scenarios. If you do not find a task to suit your needs then it is easy to create a custom NAnt task. I hope this article offers some tips for managing builds to multiple target environments.

Source Code