I use MSBuild and CruiseControl.NET for my automated build process.  Just yesterday I was debugging a problem in one of my assemblies and came across this warning message:

Use command line option '/keyfile' or appropriate project settings instead of 'AssemblyKeyFile'

I had never noticed the warning before, but looked into some of the past builds and noticed that it had always been there.  After doing some research, I found out that Visual Studio 2005 now considers the use of setting the AssemblyKeyFile in the AssemblyInfo.cs file a security issue.  This is due to the AssemblyKeyFile attribute being embedded within your assembly and possibly containing sensitive path information.  The warning also recommended setting this information via the project settings.  I use an external strong name key file for all of my assemblies.  It resides outside all of my projects/solutions.  When I referenced this strong name key file, VS2005 copied it into the project.  This wasn't a good thing at all.

After looking into what was going on, I noticed that in the project msbuild file, there is a property called AssemblyOriginatorKeyFile.  VS2005 automatically set it to reference the copied strong name key that it placed in the project.  I updated this path to reference my external strong name key, saved it, and deleted the local copy of the strong name key in the project.  I tested the build and it worked!

I have no idea why Microsoft designed it this way, but thankfully this method works great.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

I finally managed to devote some time and finish my follow-up article on MSBuild. This time around I went over how to integrate MSBuild with CruiseControl.NET to create an automated build process. I explain each section of the build file as well as the CC.NET server configuration file. I also provide the Visual Studio solution and CC.NET ccnet.config files for download.

You can read the article here.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

In my last article, I talked about using MSBuild with Web Deployment Projects (WDP), an add-in to Visual Studio that allows you to create MSBuild files for web projects. WDP enables you to manage not only build configuration and merge options, but other tasks such as specifying changes for the application's Web.config file during compilation, changing connection strings, creating virtual directories, and performing other tasks at specific points in the deployment process. Although this adds a lot of flexibility I have found that Web Application Projects (WAP) offers a better solution. WAP provides another web project model option for Visual Studio 2005 that can be used as an alternative to the built-in web-site based solution. It uses the same project, build and compilation semantics as the Visual Studio 2003 web project model. Specifically (from the web site):

  • All files contained within the project are defined within a project file (as well as the assembly references and other project meta-data settings). Files under the web's file-system root that are not defined in the project file are not logically considered part of the web project.
  • All code files within the project are compiled into a single assembly (that gets built and persisted in the \bin directory on each compile).
  • The compilation system uses a standard MSBuild based compilation process. This can be extended and customized using standard MSBuild extensibility rules. You can therefore control the build through the property pages, so for example, you can name the output assembly.

Installing WAP does not modify or affect the behavior of Visual Studio 2005, it simply adds a new project type to Visual Studio. WAP is still in beta and there are some known issues that you should read and be aware of before using it. A new version is due out this month that will fix most of the bugs and problems with the existing version. I will be using WAP in this article as my web project type.

On my build server I'm running Windows Server 2003 with IIS 6.0 with CruiseControl.NET 1.0.1251 installed. You can download CruiseControl.NET here if you don't have it installed. For source control I'm going to use Visual SourceSafe 2005, although you could plug in any source control system (Vault, Subversion, etc.). I'll also be using the MSBuild Community Tasks Project library. The current version is 1.0.0.29, but I'm using a newer version that hasn't been released since I'm a contributing member of the project. You can download the latest version of the source code from the project home page, but you'll have to build the project once you download it. A new version of the library is due out very soon, so stay tuned.

Configuring CruiseControl.NET

Previous to .NET 2.0 being released, I used CruiseControl.NET (CC.NET) with NAnt, both of which are freely available as open source projects. MSBuild and NAnt are both build tools that allow you to perform a myriad of tasks from building solutions to running unit tests, zipping up the output of builds, transforming XML with XSLT, accessing source control, sending email, etc. Both tools are extremely powerful. Now that .NET 2.0 has been released, MSBuild has become the build tool of choice for Visual Studio solutions. After all, the project files that are created by Visual Studio are created for MSBuild. What I am going to do is show you how I have integrated MSBuild with CC.NET to create a continuous integration process.

The continuous integration process I'm going to emulate will look like the following:


The trigger for CC.NET to launch a build will be the source control repository. CC.NET offers several different Trigger blocks that control how a build will get launched. Some of the different types can be an Interval Trigger, a Scheduled Trigger, or a Filter Trigger. The most common trigger used is the Interval Trigger. The Interval Trigger allows you to specify that a build should be run periodically, after a certain amount of time. By default, a build will only be triggered if source control modifications have been detected since the last build. The trigger can also be configured to force a build even if no changes have occurred to the source control repository. Another nice feature is that you can nest trigger types together. For example, you can use a Filter Trigger to specify that a build should not be performed in a certain time period. You can then nest an Interval Trigger to specify the normal build conditions. Here is an example of this in the CC.NET's server configuration file (I'll refer to this as ccnet.config from now on):

    9     <triggers>

   10       <filterTrigger startTime="0:00" endTime="6:00">

   11         <trigger type="intervalTrigger" seconds="300" />

   12       </filterTrigger>

   13     </triggers>


The outer Filter Trigger indicates that the build should not occur from midnight until six in the morning. The inside Interval Trigger then specifies that the build should occur every five minutes.

The next step I need to setup in the ccnet.config file is the retrieval of the source code. Before I show that, I'd like to talk about the structure of the Visual Studio solution. This is important because when CC.NET retrieves the source code it will need to know where the master MSBuild project file resides. Here is a quick glance of the solution in Visual Studio:



There are four projects, a data layer, business layer, presentation layer, and a unit test project. I have also specified a folder for solution items. In this folder I will place the master MSBuild project file, along with other solution-wide items, including a sample strong name key. The project structure inside VSS looks like the following:



When CC.NET gets the latest version of the solution from VSS, it will create a file structure exactly like this. Now that you know where the main MSBuild project file will be when you perform the GET from VSS, you can then specify the location in the ccnet.config file. Here are the SourceControl and MSBuild blocks:

  243     <sourcecontrol type="vss">

  244       <executable>C:\Program Files\Microsoft Visual SourceSafe\ss.exe</executable>

  245       <project>$/DougRohm/Articles/MSBuildAndCCNet/</project>

  246       <username>msbuild</username>

  247       <password>msbuildpass</password>

  248       <ssdir>D:\VSS</ssdir>

  249       <workingDirectory>D:\CCNET_Build\DougRohm\Articles\MSBuildAndCCNet\Source\Dev</workingDirectory>

  250       <applyLabel>true</applyLabel>

  251       <autoGetSource>true</autoGetSource>

  252     </sourcecontrol>

  253 

  254     <tasks>

  255       <msbuild>

  256         <executable>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe</executable>

  257         <workingDirectory>D:\CCNET_Build\DougRohm\Articles\MSBuildAndCCNet\Source\Dev</workingDirectory>

  258         <projectFile>DougRohm.Articles.MSBuildAndCCNet.proj</projectFile>

  259         <buildArgs>/p:BuildType=Dev</buildArgs>

  260         <targets>Build</targets>

  261         <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files\CruiseControl.NET\webdashboard\bin\ThoughtWorks.CruiseControl.MsBuild.dll</logger>

  262       </msbuild>

  263     </tasks>


The source control task gets the entire solution from VSS and places it into the value of the workingDirectory property. Once that is complete, CC.NET launches the MSBuild task by pointing the workingDirectory and projectFile properties to the source code. I'm also passing a property to the MSBuild project file through the buildArgs property. I use this property in the MSBuild project file to determine certain property settings. I also set the default target so that the MSBuild project file will know which target to execute first.

Configuring the MSBuild Project File

In the MSBuild project file, the first thing I check is the BuildType property that is passed from CC.NET so that I can initialize certain properties for the build:

    3   <!-- BuildType Properties -->

    4   <Choose>

    5     <When Condition="'$(BuildType)' == 'Dev'">

    6       <PropertyGroup>

    7         <DeploymentFolder>C:\Inetpub\Dev</DeploymentFolder>

    8         <DeploymentDocFolder>C:\Inetpub\Dev_Docs</DeploymentDocFolder>

    9         <Configuration>Debug</Configuration>

   10         <DebugSymbols>true</DebugSymbols>

   11         <VersionBuildType>Date</VersionBuildType>

   12         <VersionRevisionType>Increment</VersionRevisionType>

   13       </PropertyGroup>

   14     </When>

   15     <When Condition="'$(BuildType)' == 'Stage'">

   16       <PropertyGroup>

   17         <DeploymentFolder>C:\Inetpub\Stage</DeploymentFolder>

   18         <Configuration>Debug</Configuration>

   19         <DebugSymbols>true</DebugSymbols>

   20         <VersionBuildType>NonIncrement</VersionBuildType>

   21         <VersionRevisionType>NonIncrement</VersionRevisionType>

   22       </PropertyGroup>

   23     </When>

   24     <When Condition="'$(BuildType)' == 'QA'">

   25       <PropertyGroup>

   26         <DeploymentFolder>C:\Inetpub\QA</DeploymentFolder>

   27         <Configuration>Release</Configuration>

   28         <DebugSymbols>false</DebugSymbols>

   29         <VersionBuildType>NonIncrement</VersionBuildType>

   30         <VersionRevisionType>NonIncrement</VersionRevisionType>

   31       </PropertyGroup>

   32     </When>

   33     <When Condition="'$(BuildType)' == 'Prod'">

   34       <PropertyGroup>

   35         <DeploymentFolder>C:\Inetpub\Prod</DeploymentFolder>

   36         <Configuration>Release</Configuration>

   37         <DebugSymbols>false</DebugSymbols>

   38         <VersionBuildType>NonIncrement</VersionBuildType>

   39         <VersionRevisionType>NonIncrement</VersionRevisionType>

   40       </PropertyGroup>

   41     </When>

   42   </Choose>


After determining the value of the 'BuildType' property, I set the deployment folder that the build will use for deployment, the configuration type, whether or not to produce debug symbols, and two values for the 'VersionBuildType' and 'VersionRevisionType'. These two values will be used by the Version task when I strong name the assemblies. I then proceed to setup several other properties and item groups that I will use during the build. I'll go through each one:

   43   <PropertyGroup>

   44     <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>

   45     <!-- Default build group is 'Build', other is 'NUnit' - See ItemGroup <VSProjects> -->

   46     <BuildGroup Condition="'$(BuildGroup)' == ''">Build</BuildGroup>

   47     <SourcePath>$(MSBuildProjectDirectory)</SourcePath>

   48     <LibraryPath>$(SourcePath)\Libraries</LibraryPath>

   49     <DocumentationPath>$(SourcePath)\Documentation</DocumentationPath>

   50     <BuildPath>$(SourcePath)\Compilation\$(Configuration)</BuildPath>

   51     <UnitTestPath>$(SourcePath)\Compilation\UnitTests</UnitTestPath>

   52     <ProjectNamespace>DougRohm.Articles.MSBuildAndCCNet</ProjectNamespace>

   53     <BuildArchiveFolder>$(MSBuildProjectDirectory)\..\..\Builds\$(BuildType)</BuildArchiveFolder>

   54 

   55     <VssUsername>msbuild</VssUsername>

   56     <VssPassword>msbuildpass</VssPassword>

   57     <VssDatabasePath>\\MyServer\VSS$\srcsafe.ini</VssDatabasePath>

   58 

   59     <NUnitPostfix>-results.xml</NUnitPostfix>

   60     <NUnitReportXslFilename>NUnitReport.xsl</NUnitReportXslFilename>

   61 

   62     <DeveloperBuild Condition="'$(DeveloperBuild)' == ''">False</DeveloperBuild>

   63     <ImportTargets>$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets</ImportTargets>

   64   </PropertyGroup>


In this property group I'm setting up a number of properties that deal with directory paths, source control username/password, and NUnit processing. I also add a property for the .targets file for the MSBuild Community Tasks Project library. This allows me to have access to all of the MSBuild tasks in that library without having to add a UsingTask element for every task I need to use. I then call the Import element to import all of the tasks:

   66 <Import Condition="Exists($(ImportTargets))" Project="$(ImportTargets)" />


The directory structure on the build server that CC.NET will use looks like the following:



There are a few more items I create before any of the targets are executed. I create two item groups for cleaning the build directories. The first, CleanOutput, cleans the output directories after the projects are compiled. The second item group, CleanSource, is a collection of the directories to clean before compilation takes place. Here are the item groups:

  101   <ItemGroup>

  102     <CleanOutput Include="$(BuildPath)" />

  103     <CleanOutput Include="$(DocumentationPath)" />

  104     <CleanOutput Include="$(UnitTestPath)" />

  105   </ItemGroup>

  106 

  107   <ItemGroup>

  108     <CleanSource Include="$(LibraryPath)" />

  109     <CleanSource Include="$(SourcePath)\Business" />

  110     <CleanSource Include="$(SourcePath)\Data" />

  111     <CleanSource Include="$(SourcePath)\Tests" />

  112     <CleanSource Include="$(SourcePath)\Web.UI" />

  113   </ItemGroup>


I then create an item group for all of the projects in the solution:

  115   <!-- VS Projects -->

  116   <ItemGroup>

  117     <VSProjects Include="$(SourcePath)\Data\$(ProjectNamespace).Data.csproj">

  118       <Group>Build</Group>

  119       <Title>$(ProjectNamespace).Data</Title>

  120       <Description>Data layer.</Description>

  121     </VSProjects>

  122 

  123     <VSProjects Include="$(SourcePath)\Business\$(ProjectNamespace).Business.csproj">

  124       <Group>Build</Group>

  125       <Title>$(ProjectNamespace).Business</Title>

  126       <Description>Business layer.</Description>

  127     </VSProjects>

  128 

  129     <VSProjects Include="$(SourcePath)\Web.UI\$(ProjectNamespace).Web.UI.csproj">

  130       <Group>Build</Group>

  131       <Title>$(ProjectNamespace).Web.UI</Title>

  132       <Description>Web presentation layer.</Description>

  133     </VSProjects>

  134 

  135     <VSProjects Include="$(SourcePath)\Tests\$(ProjectNamespace).Tests.csproj">

  136       <Group>NUnit</Group>

  137       <Title>$(ProjectNamespace).Tests</Title>

  138       <Description>Unit test assembly.</Description>

  139     </VSProjects>

  140   </ItemGroup>


For each VSProjects item, I'm also declaring metadata values that I can access during the build. The 'Group' metadata value allows me to separate regular build projects from NUnit test projects. I added that option in case I wanted to run the build file manually and only run the unit tests. This was purely optional and for this article I'll only be using the Build projects (although I do run the unit tests later). I'm also setting metadata for the 'Title' and 'Description' for each project. These values are used for when I strong name the assembly (I'll get to that in a minute).

I then create an item group that I'll use when building the NUnit projects. This collection represents anything that I'll need for generating the NUnit report. These items will get copied to the NUnitTestPath folder declared earlier.

  142   <ItemGroup>

  143     <!-- Content files to copy to the test directory -->

  144     <!-- Provide metadata <SubDirectory> for relative destination directory -->

  145     <NUnitContents Include="$(SourcePath)\$(NUnitReportXslFilename)">

  146       <SubDirectory>$(ProjectNamespace).Tests</SubDirectory>

  147     </NUnitContents>

  148   </ItemGroup>


The last item group I declare is the XSL file that will be used to generate the NUnit report.

  150   <ItemGroup>

  151     <NUnitReportXslFile Include="$(SourcePath)\$(NUnitReportXslFilename)">

  152       <project>$(ProjectNamespace)</project>

  153       <configuration>$(Configuration)</configuration>

  154       <msbuildFilename>$(MSBuildProjectFullPath)</msbuildFilename>

  155       <msbuildBinpath>$(MSBuildBinPath)</msbuildBinpath>

  156       <xslFile>$(SourcePath)\$(NUnitReportXslFilename)</xslFile>

  157     </NUnitReportXslFile>

  158   </ItemGroup>


As I mentioned earlier, you can pass the targets to execute in the MSBuild file from the CC.NET configuration file (ccnet.config) through the 'buildArgs' property. This allows you to call a specific target in the build file, which in this case, is 'Build'. To give you a quick overview of the targets I have in my build file I have them listed here:

1) Build
    2) AssemblyInfo
        3) CheckoutVersionFile
            4) CreateWorkingFolders
5) ParseTokens
6) UnitTest
7) DeployBuild
8) Documentation

I numbered the targets to denote the order in which they are executed. Build is called first, but it depends on AssemblyInfo. AssemblyInfo depends on CheckoutVersionFile. CheckoutVersionFile depends on CreateWorkingFolders. Based on all of that, CreateWorkingFolders executes first, then CheckoutVersionFile, then AssemblyInfo, and then finally Build. The Build target calls ParseTokens and then proceeds to build all of the projects in the solution. After compilation, the Build target then calls the UnitTest target and then DeployBuild. DeployBuild calls the Documentation target internally.

Here is the listing for the Build, AssemblyInfo, CheckoutVersionFile, and CreateWorkingFolders targets:

  160   <Target Name="CreateWorkingFolders" Condition="'$(BuildGroup)' == 'Build'">

  161     <!-- Clean the output folders -->

  162     <RemoveDir Directories="@(CleanOutput)" />

  163 

  164     <MakeDir Condition="!Exists('$(DocumentationPath)')" Directories="$(DocumentationPath)" />

  165     <MakeDir Condition="!Exists('$(BuildPath)')" Directories="$(BuildPath)" />

  166     <MakeDir Condition="!Exists('$(UnitTestPath)')" Directories="$(UnitTestPath)" />

  167   </Target>

  168 

  169   <Target Name="CheckoutVersionFile" Condition="'$(BuildGroup)' == 'Build'"

  170       DependsOnTargets="CreateWorkingFolders">

  171     <!-- Checkout BuildNumber.txt -->

  172     <VssCheckout UserName="$(VssUsername)"

  173           Password="$(VssPassword)"

  174           LocalPath="$(SourcePath)"

  175           Recursive="False"

  176           DatabasePath="$(VssDatabasePath)"

  177           Path="$/DougRohm/Articles/MSBuildAndCCNet/Version.txt" />

  178 

  179     <!-- Get Build and Revision number -->

  180     <Version VersionFile="$(SourcePath)\Version.txt" BuildType="$(VersionBuildType)" RevisionType="$(VersionRevisionType)">

  181       <Output TaskParameter="Major" PropertyName="Major" />

  182       <Output TaskParameter="Minor" PropertyName="Minor" />

  183       <Output TaskParameter="Build" PropertyName="Build" />

  184       <Output TaskParameter="Revision" PropertyName="Revision" />

  185     </Version>

  186     <Message Text="Version: $(Major).$(Minor).$(Build).$(Revision)" />

  187   </Target>

  188 

  189   <Target Name="AssemblyInfo" Condition="'$(BuildGroup)' == 'Build'"

  190       DependsOnTargets="CheckoutVersionFile" Inputs="@(VSProjects)"

  191       Outputs="%(RootDir)%(Directory)\Properties\AssemblyInfo.cs">

  192 

  193     <!-- For each VS project, create the AssemblyInfo.cs file  -->

  194     <AssemblyInfo CodeLanguage="CS"

  195             OutputFile="%(VSProjects.RootDir)%(Directory)Properties\AssemblyInfo.cs"

  196             ComVisible="false"

  197             CLSCompliant="false"

  198             AssemblyCompany="DougRohm.com"

  199             AssemblyProduct="MSBuild and CruiseControl.NET"

  200             AssemblyCopyright="Copyright © 2005-2006 Douglas Rohm"

  201             AssemblyTitle="%(Title)"

  202             AssemblyDescription="%(Description)"

  203             AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"

  204             AssemblyKeyFile="..\..\..\DougRohm.Articles.snk" />

  205   </Target>

  206 

  207   <Target Name="Build" DependsOnTargets="AssemblyInfo">

  208     <CallTarget Targets="ParseTokens" />

  209 

  210     <MSBuild Projects="@(VSProjects)" Condition="'%(Group)' == '$(BuildGroup)'"

  211         Properties="Configuration=$(Configuration)">

  212       <Output TaskParameter="TargetOutputs" ItemName="BuildTargetOutputs"/>

  213     </MSBuild>

  214 

  215     <Copy SourceFiles="@(BuildTargetOutputs)"

  216         DestinationFolder="$(BuildPath)\bin"

  217         Condition="'$(BuildGroup)' == 'Build'"

  218         SkipUnchangedFiles="true" />

  219     <Copy SourceFiles="@(BuildTargetOutputs->'%(RootDir)%(Directory)%(Filename).pdb')"

  220         DestinationFolder="$(BuildPath)\bin"

  221         Condition="'$(BuildGroup)' == 'Build' And '$(Configuration)' == 'Debug'"

  222         SkipUnchangedFiles="true" />

  223     <Copy SourceFiles="@(BuildTargetOutputs->'%(RootDir)%(Directory)%(Filename).xml')"

  224         DestinationFolder="$(BuildPath)\bin"

  225         Condition="'$(BuildGroup)' == 'Build'"

  226         SkipUnchangedFiles="true" />

  227 

  228     <!-- Run NUnit tests and generate report -->

  229     <CallTarget Targets="UnitTest" />

  230 

  231     <!-- Deploy project -->

  232     <CallTarget Targets="DeployBuild" />

  233   </Target>


The first thing that happens is CreateWorkingFolders cleans out the BuildPath, DocumentationPath, and UnitTestPath folders by deleting the folders and then recreating them. Next, the CheckoutVersionFile target checks out the version file (Version.txt). The version file is used by the Version task so that it can reference it to generate the next version for the build. The version file resides in the Solution Items folder in Visual Studio. I then generate the new version by executing the Version task. Notice that I'm using the VersionBuildType and VersionRevisionType properties that I set in the Choose element at the beginning of the build file. Next, the AssemblyInfo target executes and generates the AssemblyInfo.cs file with all of the appropriate assembly attributes and places it in the projects Properties subfolder. The AssemblyInfo target has both Inputs and Outputs parameters. The Inputs parameter is a collection of all the Visual Studio projects that need to be built. The Outputs parameter is the AssemblyInfo.cs file that will replace the existing AssemblyInfo.cs file in the Properties directory.

At this point, tasks in the Build target can execute. The first thing the Build target does is call the ParseTokens target. This target is used to do any last minute modifications to the source files before compilation. In the Web.UI project there is the Default.aspx page. Near the bottom of the page there are two tokens I added for the application version and build date (@VERSION@ and @DATE@). The ParseTokens target will replace these tokens with the values I need. The first thing I do is create an item group named ParseIncludeFiles that contains all the files I want to parse, excluding the files I want to ignore. One thing I need to mention at this point is that I'm creating this item group on the fly, as opposed to creating it with all the other properties and item groups at the beginning of the build file. The reason for this is because all properties and item groups are evaluated before any targets. If I had declared this item group before the target, no output files would be in the source directory to be included in the item group (or excluded in this instance).

After I create the item group of files to parse, I get the current timestamp of the build using the Time task. I then call the FileUpdate task passing the ParseIncludeFiles item group along with the RegEx pattern for @VERSION@ and the replacement text. The replacement text is the $(Major), $(Minor), $(Build), and $(Revision) properties that were calculated in the CheckoutVersionFile target. Next, I call FileUpdate again passing in the ParseIncludeFiles item group with a RegEx pattern for @Date@ and it's replacement text. The replacement text for @DATE@ is the value from the Time task, $(BuildDate).

The last step in the ParseTokens target is a call to the Script task that will execute a custom script that I have declared with the other properties at the beginning of the build file:

   68   <PropertyGroup>

   69     <UpdateWebConfigCode>

   70       <![CDATA[

   71         public static void ScriptMain()

   72         {

   73           XmlDocument wcXml = new XmlDocument();

   74           wcXml.Load(@"Web.UI\Web.config");

   75 

   76           XmlElement root = wcXml.DocumentElement;

   77           XmlNodeList connList = root.SelectNodes("//connectionStrings/add");

   78           XmlElement elem;

   79 

   80           foreach (XmlNode node in connList)

   81           {

   82             elem = (XmlElement)node;

   83 

   84             switch (elem.GetAttribute("name"))

   85             {

   86               case "Northwind":

   87                 elem.SetAttribute("connectionString", "server=stageServer;database=Northwind;UID=web;PWD=pass");

   88                 break;

   89               case "pubs":

   90                 elem.SetAttribute("connectionString", "server=stageServer;database=pubs;UID=web;PWD=pass");

   91                 break;

   92             }

   93           }

   94 

   95           wcXml.Save(@"Web.UI\Web.config");

   96         }

   97       ]]>

   98     </UpdateWebConfigCode>

   99   </PropertyGroup>


I added this task call as an example on request from an email I got from a reader. This task will execute the code in the UpdateWebConfigCode property. The code will replace the connectionStrings in the web.config file so that it can be used on another web server after deployment. When the project is deployed from the dev server to the staging server the connectionStrings will be updated and no manual modification will be needed. Again, this is just an example of how you could modify specific configuration settings with MSBuild.

Now that ParseTokens is complete, the next step in the Build target is to run MSBuild on the projects in the VSProjects item group. There is a condition on the MSBuild task call to only build the project if the item 'Group' is equal to the BuildGroup property, which is set to Build by default. The MSBuild task call also passes the Configuration property and creates an output item group called BuildTargetOutputs. After the MSBuild task call completes building each project, I copy the BuildTargetOutputs to the $(BuildPath)\bin folder. I also copy the .pdb files to the same directory if the Configuration is set to Debug. Lastly, I copy the XML documentation file to the same folder. All of this happens for all the projects in the VSProjects item group.

The next task to run in the Build target is to run the unit tests. I do this by calling the UnitTest target directly. There is a condition on the UnitTest target that checks the BuildType property and will only run if the BuildType is set to 'Dev'. In my build scenario, I only want to run my unit tests when building for the Dev environment. If you want to run your unit tests in other environments simply modify this condition. Here is the target with explanation below:

  270   <Target Name="UnitTest" Condition="'$(BuildType)' == 'Dev'">

  271     <!-- Set BuildGroup to 'NUnit' -->

  272     <CreateProperty Value="NUnit">

  273       <Output TaskParameter="Value" PropertyName="BuildGroup" />

  274     </CreateProperty>

  275 

  276     <!-- Build the VSProjects of the 'NUnit' BuildGroup -->

  277     <MSBuild Projects="@(VSProjects)" Condition="'%(Group)' == '$(BuildGroup)'"

  278         Properties="Configuration=$(Configuration);BuildGroup=NUnit