Monday, September 10, 2007

ASP.NET web application's build using NAnt

Introduction

Now a days nightly builds are the most important entity for any project development lifecycle. NAnt is one of the
most popular tool for .NET automated builds. NAnt is same as Ant but it is specially designed for .NET.
This article will explain the procedure for writing the build file for ASP.NET application in brief.
I am expecting that reader knows the basics of XML technology and deployment of ASP.NET application.

Background

The deployment of ASP.NET application is little bit tricky. When you build the ASP.NET application
it generates separate .dll file for each class library included in the project. However ASP.NET 2.0 does not generate .dll file for your code behind
files i.e. ".cs" in case of C#. We have to use "aspnet_compiler.exe", which comes along with .NET 2.0 framework for generating .dll files for .cs files.
Again it generates separate .dll for each .cs. All this makes build and deployment process very complicated.

Here I am trying to guide you for writing build file for ASP.NET application's automated build using NAnt.
NAnt is very popular build tool. Its open source and you can download it freely from sourceforge.net

To use this code you have to download some external tools to setup your dev environment.
1. NAnt
2. Visual Studio 2005 Web Deployment Projects

Using the code

As mentioned earlier you have to first install the "Visual Studio 2005 Web Deployment Projects". Just download it from MSDN site and
install it on default location. This installation is required to merge multiple dll files generated by MSBuild.exe.
Download NAnt and copy the folder in to "c:"( You can copy it in to any directory. I am giving "C:" just for an example. I have used the same path in
the build file given below.)
Now go to "C:\nant\nant-0.85\bin" folder. Here you can see set of some xml/xsl files and one "lib" folder.
Create one new file here and name it as "build.number.xml". This file will specify the current version of the project in the source control.
Copy the following contents and save the file.



<?xml version="1.0" encoding="utf-8"?>
<build>
<major>1</major>
<minor>0</minor>
<build>0</build>
<release>0</release>
<companyname>CompanyName</companyname>
<copyright>Copyright 2007</copyright>
</build>


The schema of the above file should be in the same folder. Create one file and name it as "genVer.xsl".
Copy the following code and save the file.



<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">

#define VER <xsl:value-of select="build/major"/>,<xsl:value-of select="build/minor"/>,<xsl:value-of select="build/build"/>,<xsl:value-of select="build/release"/>

#define STRVER "<xsl:value-of select="build/major"/>,<xsl:value-of select="build/minor"/>,<xsl:value-of select="build/build"/>,<xsl:value-of select="build/release"/>"

#define COMPANYNAME "<xsl:value-of select="build/companyname"/>"

#define COPYRIGHT "<xsl:value-of select="build/copyright"/>"

</xsl:template>
</xsl:stylesheet>



Now we have to do some xsl coding which will help us to increment the version number. So create one new file in the same folder and
name it as "IncrBuild.xsl"
Copy the following code and save the file.


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/build/build">
<xsl:variable name="buildNumber" select="."/>
<xsl:element name="build">
<xsl:value-of select="$buildNumber + 1"/>
</xsl:element>
</xsl:template>
<xsl:template match="/ @* node()">
<xsl:copy>
<xsl:apply-templates select="@* node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>



Now you are ready to write actual build file. by default "nant" folder contains one file "Nant.build". This is simple xml file where you
can write you build code. You can separate file for your build code with extension ".build". You can run this build code using "nant.exe".
There are several options available with this exe file. You can read it from NAnt documentation available <a href="http://nant.sourceforge.net/release/0.85/help/">here</a>.
I am not giving NAnt coding basics here. You can read it from NAnt's help.
So lets start writing the heart beats for your automated build file.
Open "NAnt.build" file in any text or XML editor.

First create the rootnode for which will specify the name of the project and default target which NAnt.exe execute to start the
build process.



<project name="ProjectName" default="buildProject" xmlns="http://nant.sf.net/schemas/nant-0.85.win32.net-1.0.xsd">
</project>


Between the project tag, we have write some targets and properties.
To make our code more generic and configurable we have to declare some properties first.Please see code comments for description.



<!--Properties for build process-->
<!-- BuildDir is the directory where our .NET source resides.Project directory for web application-->
<property name="BuildDir" value="D:\Work"/>
<!-- target is the directory where we are going to store deployable files.-->
<property name="target" value="D:\Work\deploy" overwrite="false" />
<!-- SolutionFileName is the name of your solution file.-->
<property name="SolutionFileName" value="NantTrial.sln"/>
<!-- BuildNumberPath is path of the xml file where your current build no is specified.-->
<property name="BuildNumberPath" value="C:\nant\nant-0.85\bin\build.number.xml"/>
<!-- dotnet is the path of the folder where .NET framework is installed.-->
<property name="dotnet" value="c:/windows/Microsoft.NET/Framework/v2.0.50727" overwrite="false"/>
<!-- MSBuildPath is the path for the MSBuild.exe. This file comes along with .NET framework and
being stored at the same folder.-->
<property name="MSBuildPath" value="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe" />
<!-- WebDeploymentExePath is the path of the folder where you have installed WebDeployment Project.-->
<property name="WebDeploymentExePath" value="C:\Program Files\MSBuild\Microsoft\WebDeployment\v8.0" />
<!-- WebProjectFolderName is your project's directory name-->
<property name="WebProjectFolderName" value="NantTrial" />
<!-- FinalDeployDllName is name which you want to give your final .dll file generated by WebDeployment tool.-->
<property name="FinalDeployDllName" value="ProjectName" />
<!-- newReleaseLabel can be used for naming the folder which contains final deployable files or labelling the source in
source control.-->
<property name="newReleaseLabel" value="ProjectName-0-0-0-0"/>
<!--Major build number-->
<property name="build.version.major" value="0"/>
<!--Minor build number-->
<property name="build.version.minor" value="0"/>
<!--build number-->
<property name="build.version.build" value="0"/>
<!--revision number-->
<property name="build.version.revision" value="0"/>
<!--Ending Properties for build process-->



You can configure above properties as per your requirements.
There are total 4 targets in the build file.
1. buildProject [Default target responsible for build the .NET application and create .dll files in the deploy folder.]
2. clean [Remove the previous deploy folder]
3. setversion [Set version string in application's AssemblyInfo.cs file.]
4. setVersionString [Set version string property. This property can be used for labelling the source or naming the deply folder.]
5. incrementBuildNumber [This will execute .xsl file to increment current version by 1]

All these properties depend on each other and will be executed in the bottom-up manner (5 to 1). You can configure this as per your
requirements using target's "depends" property.

You can download the attached sample NAnt.build file for above targets.
The key part of the article lies in "buildProject" target. Here we are going to build the web application.
To build the application we have to use .NET's command line utility "MSBuild.exe". This file lies in .NET framework's folder by default.
This file takes solution file name as argument. Execute it using following code.


<exec program="${MSBuildPath}">
<arg value="${BuildDir}\${SolutionFileName}" />
</exec>



Now the code can be compiled using "aspnet_compiler.exe". This file also comes along with .NET framework installation and resides
in the same folder."aspnet_compiler.exe" has multiple command line options. You can use it as per your need.
The complete set of options are described <a href="http://msdn2.microsoft.com/en-us/library/ms229863(VS.80).aspx">here</a>.
Execute "aspnet_compiler.exe" using following code.



<exec program="${dotnet}/aspnet_compiler.exe" commandline="-f -u -p ${BuildDir}\${WebProjectFolderName} -v / ${target}"/>


This tool will generate separate .dll for your class libraries. But the problem is it also generates separate .dlls for every .cs file.
It not a good idea to ship your build with these multiple .dll files, one for each web page. So there is need to merge these
multiple .dll files in to one final deployable .dll.

You can do this using Visual Studio 2005 Web Deployment Projects.
Installation of Web Deployment Projects copies "aspnet_merge.exe" to your build machine. It takes target folder name and name for
final dll as an arguments.

Execute "aspnet_merge.exe" using following code.



<exec program="${WebDeploymentExePath}\aspnet_merge.exe" commandline="${target} -o ${FinalDeployDllName} -xmldocs"/>



This whole process will copy some unnecessary files to target deployment folder which we need to remove.
Remove these files using



<delete>
<!--Remove unnecessary files from the build.-->
<fileset>
<include name="${target}/*.build" />
<include name="${target}/*.scc" />
<include name="${target}/*.sln" />
<include name="${target}/build.*" />
</fileset>
</delete>



Now open the command prompt. Change directory to your NAnt folder (i.e."C:\nant\nant-0.85\bin\" in my case ).
type "Nant.exe" and press "Enter".
The build process will start. You can see the verbose on command line. If build gets succeded it will give message as




BUILD SUCCEEDED

Total time: 33.3 seconds.


Now check your "target" folder i.e. value of property "target" in build file. In my case it is "D:\Work\deploy".
All web pages (.aspx) should be there with one "bin" folder and "precompiledApp.config" file. Bin folder should contain
all dlls for class libraries and one dll for your ".cs" file instead of seperate dlls for each. ( The name of the dll should be
the value of property "FinalDeployDllName". In my case it is "ProjectName" )

Just rock!! You are now ready for deployment.

The main advantage of NAnt automated builds is you can schdule your nightly builds. Just create one "bat" file
with the command "NAnt.exe" and schedule its running time using Windows scheduler.


Points of Interest

While designing an ASP.NET application one thing keep in mind that don't keep your solution file in the same folder where
your web pages are being stored. "MSBuild.exe" will fail to perform build in this case.
I am planning to write separate article for NAnt's common target's like Source control tasks, send mail, copy files to folders on
server etc. I will out it shortly.

Whole Contents of NAnt.Build



<?xml version="1.0"?>

<project name="ProjectName" default="buildProject" xmlns="http://nant.sf.net/schemas/nant-0.85.win32.net-1.0.xsd">
<!--Properties for build process-->
<!-- BuildDir is the directory where our .NET source resides.Project directory for web application-->
<property name="BuildDir" value="D:\Work"/>
<!-- target is the directory where we are going to store deployable files.-->
<property name="target" value="D:\Work\deploy" overwrite="false" />
<!-- SolutionFileName is the name of your solution file.-->
<property name="SolutionFileName" value="NantTrial.sln"/>
<!-- BuildNumberPath is path of the xml file where your current build no is specified.-->
<property name="BuildNumberPath" value="C:\nant\nant-0.85\bin\build.number.xml"/>
<!-- dotnet is the path of the folder where .NET framework is installed.-->
<property name="dotnet" value="c:/windows/Microsoft.NET/Framework/v2.0.50727" overwrite="false"/>
<!-- MSBuildPath is the path for the MSBuild.exe. This file comes along with .NET framework and
being stored at the same folder.-->
<property name="MSBuildPath" value="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe" />
<!-- WebDeploymentExePath is the path of the folder where you have installed WebDeployment Project.-->
<property name="WebDeploymentExePath" value="C:\Program Files\MSBuild\Microsoft\WebDeployment\v8.0" />
<!-- WebProjectFolderName is your project's directory name-->
<property name="WebProjectFolderName" value="NantTrial" />
<!-- FinalDeployDllName is name which you want to give your final .dll file generated by WebDeployment tool.-->
<property name="FinalDeployDllName" value="ProjectName" />
<!-- newReleaseLabel can be used for naming the folder which contains final deployable files or labelling the source in
source control.-->
<property name="newReleaseLabel" value="ProjectName-0-0-0-0"/>
<!--Major build number-->
<property name="build.version.major" value="0"/>
<!--Minor build number-->
<property name="build.version.minor" value="0"/>
<!--build number-->
<property name="build.version.build" value="0"/>
<!--revision number-->
<property name="build.version.revision" value="0"/>
<!--Ending Properties for build process-->

<!--Increment the build number-->
<target name="incrementBuildNumber">
<!--Increment the number in the build number file.-->
<style style="incrBuild.xsl" in="${BuildNumberPath}" out="build.number.tmp"/>
<delete file="${BuildNumberPath}"/>
<move file="build.number.tmp" tofile="${BuildNumberPath}"/>
<!--<style style="genVer.xsl" in="${BuildNumberPath}" out="${VersionPath}"/>-->
<xmlpeek file="${BuildNumberPath}" xpath="/build/major" property="build.version.major"/>
<xmlpeek file="${BuildNumberPath}" xpath="/build/minor" property="build.version.minor"/>
<xmlpeek file="${BuildNumberPath}" xpath="/build/build" property="build.version.build"/>
<xmlpeek file="${BuildNumberPath}" xpath="/build/release" property="build.version.revision"/>
<!--You can check in the latest build file in to source control.-->
</target>

<!--Set the version string for new label. This label can be used to name the final build folder or labelling the
source in source control.-->
<target name="setVersionString" depends="incrementBuildNumber">
<property name="newReleaseLabel" value="ProjectName-${build.version.major}-${build.version.minor}-${build.version.build}-${build.version.revision}"/>
<echo message= "${newReleaseLabel}" />
</target>

<!--Set version in AssemblyInfo.cs file.This version will be stamped for generated .dll files.-->
<target name="setversion" depends="setVersionString" description="Stamp the version info onto assemblyinfo.cs files">
<foreach item="File" property="filename">
<in>
<items basedir="application">
<include name="**\AssemblyInfo.cs"></include>
</items>
</in>
<do>
<script language="C#">
<code><![CDATA[
public static void ScriptMain(Project project)
{
//FileStream file = File.Open(project.Properties["filename"], FileMode.Open, FileAccess.ReadWrite);
StreamReader reader = new StreamReader(project.Properties["filename"]);
string contents = reader.ReadToEnd();
reader.Close();
string replacement = string.Format(
"[assembly: AssemblyVersion(\"{0}.{1}.{2}.{3}\")]",
project.Properties["build.version.major"],
project.Properties["build.version.minor"],
project.Properties["build.version.build"],
project.Properties["build.version.revision"]
);
string newText = Regex.Replace(contents, @"\[assembly: AssemblyVersion\("".*""\)\]", replacement);
StreamWriter writer = new StreamWriter(project.Properties["filename"], false);
writer.Write(newText);
writer.Close();
}
]]>
</code>
</script>
</do>
</foreach>
</target>

<target name="clean" description="Remove all files from target folder.">
<delete dir="${target}" />
</target>

<target name="buildProject" depends="setversion">
<call target="clean" />
<exec program="${MSBuildPath}">
<arg value="${BuildDir}\${SolutionFileName}" />
</exec>
<exec program="${dotnet}/aspnet_compiler.exe" commandline="-f -u -p ${BuildDir}\${WebProjectFolderName} -v / ${target}"/>
<exec program="${WebDeploymentExePath}\aspnet_merge.exe" commandline="${target} -o ${FinalDeployDllName} -xmldocs"/>
<delete>
<!--Remove unnecessary files from the build.-->
<fileset>
<include name="${target}/*.build" />
<include name="${target}/*.scc" />
<include name="${target}/*.sln" />
<include name="${target}/build.*" />
</fileset>
</delete>
</target>
</project>

No comments: