In a previous post, I described some of the pain I’ve run into with using Web Deployment Projects.  I have given up on that solution and have instead rolled my own.  I’m happy to report that I now have a much better process (in my opinion), and one that didn’t require nearly as much beating my head into the keyboard. 

First, you will want to grab the MSBuild Community Tasks.  These will enable you to do some nice things like grab the revision of your local Subversion checkout and update files as you "package" them.

Now, let’s think through what we want this script to do.  We want to package up our web application in such a way that we can xcopy it to production with a minimal amount of work.  For me, this means that the script needs to create a directory structure to hold the app, its logs, and its dependencies.  It then needs to copy the web app and all of its dependencies to where they go in the directory structure.  Finally, it would be nice if the script could modify a couple of things, such as the version number displayed by the app, so that our users can report back precisely which version of the application they’re using.  Fortunately, all of these things are easy enough to do with MSBuild (and the Community Tasks). 

With that out of the way, you’re now ready to create your MSBuild script.  Start by creating an XML file with the following structure (and give it a .build extension):

   1: <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Package">
   2: ...
   3: </Project>

Note that the Project element specifies a default target of "Package".  This means that when you run MSBuild, it will look for a target named "Package" and try to execute it.  Let’s go ahead and define that target:

   1: <Target Name="Package">
   2:  
   3: </Target>

You could go ahead and run your MSBuild script now, but it wouldn’t do anything.  To make it actually do anything, we need to define tasks within the target.  So, let’s do that.  But what tasks do we need?  First, let’s copy the web application files to where they go.  Put this within the Target element:

   1: <Copy SourceFiles="@(WebAppFiles)" DestinationFolder="$(TargetDirectory)\WebApp\%(RecursiveDir)" />

There are two important things there.  First, what is "@(WebAppFiles)"?  It’s an ItemGroup that we haven’t defined yet.  We’ll get to that in just a minute.  The other thing is the value of DestinationFolder.  The "$(TargetDirectory)" refers to a property that is not defined yet (again, we’ll get to that), but the "%(RecursiveDir)" part is a special property that will insure that files in "@(WebAppFiles)" are copied as a directory tree and not as a flat file list.  If you don’t specify this, everything will go in the root of whatever DestinationFolder you specify.

So, let’s take care of "@(WebAppFiles)" real quick.  This refers to a group of files that we’ll define like so:

   1: <ItemGroup>
   2:     <WebAppFiles Include="$(WebAppDirectory)\**\*.*" />
   3: </ItemGroup>

ItemGroup elements go under your Project element, so they can be referenced across multiple tasks.  Here, we have defined a group called WebAppFiles that recursively includes all the files in the directory that the "$(WebAppDirectory)" property points to.

At this point, we’ve referenced two properties that aren’t defined yet.  Let’s go ahead and define them using a PropertyGroup:

   1: <PropertyGroup>
   2:     <TargetDirectory>.\ForDeployment</TargetDirectory>
   3:     <WebAppDirectory>.\Web</WebAppDirectory>
   4: </PropertyGroup>

Again, PropertyGroup elements go under your Project element, so they can be referenced by any Task element.  These properties just refer to directories using relative paths.

If we run this now, it’s going to fail.  Why?  Because it is trying to copy files to a directory structure that doesn’t exist yet.  Let’s create a Target to fix that:

   1: <Target Name="CreateDirectoryStructure">        
   2:     <MakeDir Directories="$(TargetDirectory)\DataFiles" />
   3:     <MakeDir Directories="$(TargetDirectory)\WebApp" />
   4:     <MakeDir Directories="$(TargetDirectory)\Logs" />
   5:     <MakeDir Directories="$(TargetDirectory)\Scripts" />
   6: </Target>

We want this to always be called before the "Package" target starts executing, so we’ll add a dependency:

   1: <Target Name="Package" DependsOnTargets="CreateDirectoryStructure">
   2: ...
   3: </Target>

At this point, we have a primitive, working MSBuild script.  Here’s the complete script:

   1: <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Package">
   2:     
   3:     <PropertyGroup>
   4:         <TargetDirectory>.\ForDeployment</TargetDirectory>
   5:         <WebAppDirectory>.\Web</WebAppDirectory>
   6:     </PropertyGroup>
   7:     
   8:     <ItemGroup>
   9:         <WebAppFiles Include="$(WebAppDirectory)\**\*.*" />
  10:     </ItemGroup>
  11:     
  12:     <Target Name="Package" DependsOnTargets="CreateDirectoryStructure">
  13:         <Copy SourceFiles="@(WebAppFiles)" DestinationFolder="$(TargetDirectory)\WebApp\%(RecursiveDir)" />
  14:     </Target>
  15:     
  16:     <Target Name="CreateDirectoryStructure">
  17:         <MakeDir Directories="$(TargetDirectory)\DataFiles" />
  18:         <MakeDir Directories="$(TargetDirectory)\WebApp" />
  19:         <MakeDir Directories="$(TargetDirectory)\Logs" />
  20:         <MakeDir Directories="$(TargetDirectory)\Scripts" />
  21:     </Target>
  22:  
  23: </Project>

That’s all for this post.  Next time, we’ll finish out the script, copying in all the necessary dependencies and even automatically setting the version number based on the revision of our Subversion working directory.