I’ve been working on a web site for a local non-profit as an opportunity to spend more time with VSTS and TFS as a user. I setup TFS 2008 SP1 in a virtual machine running on my home PC and made it accessible from the public Internet. I created the web site as an ASP.net web application project and setup an automated build that uses the Web Deployment Tool to deploy the site to a staging server. I have automated unit tests running as part of the build that validate the output of each page in the site using the W3C Markup Validation Service (inspired by Damian Edwards’ excellent Adding HTML validity checking to your ASP.net web site via unit tests post). Over the course of building this web site, I learned a lot about what you can and can’t do with VSTS and TFS and I wanted to share that (somewhat painfully) acquired knowledge with you.

Using the Web Deployment Tool (MSDeploy)

The web deployment tool consists of a console application (MSDeploy) and a Windows service that runs on either the source or target location. I wanted to invoke the tool when the build succeeds, after the output has been copied to the drop location, so I overrode the AfterDropBuild target as follows:

<Target Name="AfterDropBuild">
  <Message Text="This is the AfterBuild target" Importance="normal"/>
  <!-- Insert other tasks to run after build here -->
  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
      BuildUri="$(BuildUri)"
      Message="Deploying Website"
      Condition="'$(IsDesktopBuild)'!='true'" >
    <Output TaskParameter="Id" PropertyName="InstallerStepId" />
  </BuildStep> 

  <Exec Command="Deploy.cmd $(BuildDefinition) 
    &quot;$(DropLocation)\$(BuildNumber)\Release\_PublishedWebsites\Web&quot;"
    WorkingDirectory="$(SolutionRoot)\Source\Deploy" /> 

  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
    BuildUri="$(BuildUri)" 
    Id="$(InstallerStepId)" 
    Status="Succeeded" 
    Condition="'$(IsDesktopBuild)'!='true'" /> 

    <OnError ExecuteTargets="DeploymentFailed" />
  </Target>

  <Target Name="DeploymentFailed">
    <!-- Called if deployment of the web site fails -->
    <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
      BuildUri="$(BuildUri)"
      Id="$(InstallerStepId)"
      Status="Failed"
      Condition="'$(IsDesktopBuild)'!='true'" />
</Target>

You’ll notice that the Exec command invokes a command script. I’m not doing anything in that command script that you couldn’t do in MSBuild, but I’m a little more comfortable with batch files, so I chose to implement the deployment steps there as follows:

@echo off
if not %1==FarmersMarket-Nightly goto DEBUG

echo Deploying to production server...
"%ProgramFiles%\IIS\Microsoft Web Deploy\msdeploy.exe" 
  -verb:sync -source:contentPath="%2" 
  -dest:contentPath=C:\Inetpub\wwwroot\FarmersMarket,
    computerName=domain.com,username=domain\user,password=******** > 
    "%2\..\..\..\DeploymentLog.txt"

goto :EOF

:DEBUG
echo Deploying to stage server...
cscript %SystemRoot%\System32\iisweb.vbs /stop "Stage-WesternWakeFarmersMarket"
"%ProgramFiles%\Windows Resource Kits\Tools\robocopy" 
  "%2" C:\Stage\FarmersMarket *.* /E /MIR > "%2\..\..\..\DeploymentLog.txt"
cscript %SystemRoot%\System32\iisweb.vbs /start "Stage-WesternWakeFarmersMarket"

Usernames and passwords have, of course, been replaced with placeholder text. You’ll notice that it branches based on the name of the build definition. My continuous integration build uses robocopy to deploy the build to a test site on my TFS machine while my nightly uses msdeploy to deploy to my remote production server.

Managing the Data (or not?)

At this point, you may be wondering how I’m deploying the database. You may be disappointed to learn that, despite the fact that the msdeploy does support deployment of SQL Server database schemas, I consciously chose to avoid taking a dependency on a database at all. Instead, I chose to you a free blogging service as the ‘backing store’ for my site. I use the Syndication classes in .NET 3.5 to grab content from various feeds I’ve setup and render it on the site after caching it on the server with a rolling timeout. That single decision substantially simplified my deployment requirements. Even better, I can now make updates to much of the site’s content using my favorite blogging tool.

Automated Markup Validation

For my automated unit tests, I simply wanted to validate the XHTML generated by my pages to make sure that I hadn’t broken anything with my changes. Fortunately, Damian Edwards had already done the heavy lifting in this regard and I only needed to make some minor changes to meet my needs. Because I’m choosing to validate each page with an individual test method, I was able to leverage the TestContext object rather than pass in the name of the page to validate to the helper class. Here’s what my test method looks like:

[TestMethod()]
[HostType("ASP.NET")]
[AspNetDevelopmentServerHost("%SolutionDir%\\Web")]
[UrlToTest("http://localhost/default.aspx")]
[DeploymentItem("Web.dll")]
public void ValidateHomePage()
{
  Assert.IsTrue(TestHelper.ReturnsValidHtml(this.TestContext).IsValid,
    String.Format(Resources.W3CValidationFailure,
      TestContext.RequestedPage.AppRelativeVirtualPath));
} 

I’ve omitted the test description in the interest of brevity. Also note the user of the %SolutionDir% property where I specify the server host and the missing port number in the UrlToTest attribute (turns out that MSTest will fix-up the port number to match the one dynamically assigned to the web development server).

The next thing I tried was to turn on code coverage. I was surprised to discover that I had to build x86 assemblies in order to get code coverage data. I currently target “Any CPU” since my staging server (also my TFS) is running 32-bit Windows Server 2003 and my production server is running 64-bit Windows Server 2003. Fortunately, that particular gap is addressed in TFS 2010 and you’ll be able to capture code coverage data regardless of whether you’re targeting x86 or amd64.


If you’re interested in checking out my little side project, it’s a web site for the Western Wake Farmers’ Market. Please keep in mind that I’ve radically simplified this project in order to scope it down to something that I could realistically deliver by setting aside just a few hours a week.