Monday, November 19, 2007

Refactoring MSBuild Scripts - Extracting Common Tasks

I've spent a bit of time working with MSBuild lately, so I've been recognizing some smells with the scripts I've been writing.  One stinky thing I found myself doing, was repeating the same task over and over again.  So, in this post I thought I would highlight something that's fairly synonymous with "Extract Method" in the world of C# refactoring.

For this release, the QA team needed a bunch deployments per QA build.  Each needed a different configuration, but that said, our application always needs the same thing, despite the configuration: a database, a bunch of files, and an IIS site.  So basically what I ended up having was a bunch of common build artifacts, that only differed based upon things like file paths, database names, and backup locations.

What I want to focus on here though, are the small, one to five line tasks, that get called over and over.  Since they are such succinct tasks, I didn't feel that they warranted having their own MSBuild file, however, they were big enough to be extracted to their own tasks.

Below is a very brief example:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Default" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
    <Target Name="Default">
        <CallTarget Targets="DeployNumber1;DeployNumber2" />
    </Target>
 
    <Target Name="DeployNumber1">
        <MSBuild Projects="buildDatabase.proj" Properties="DBName=Database1" />
        <Exec Command="sqlcmd.exe -S (local) -d Database1 -Q &quot;CREATE USER [UserForInstance1] FOR LOGIN [UserForInstance1]&quot; -E -b -I" />
        <Exec Command="sqlcmd.exe -S (local) -d Database1 -Q &quot;EXEC sp_addrolemember N'db_owner', N'UserForInstance1'&quot; -E -b -I" />
        <Message Text="Deploy 1 Done" />
    </Target>
 
    <Target Name="DeployNumber2">
        <MSBuild Projects="buildDatabase.proj" Properties="DBName=Database2" />
        <Exec Command="sqlcmd.exe -S (local) -d Database2 -Q &quot;CREATE USER [UserForInstance2] FOR LOGIN [UserForInstance2]&quot; -E -b -I" />
        <Exec Command="sqlcmd.exe -S (local) -d Database2 -Q &quot;EXEC sp_addrolemember N'db_owner', N'UserForInstance2'&quot; -E -b -I" />
        <Message Text="Deploy 2 Done" />
    </Target>
 
</Project>

What this project does is two deployments of the same database, with different names and different users.   The two steps above aren't all that large, and are fairly maintainable in this context, but add a few hundred more lines of targets and tasks, and it just adds to the alphabet soup.  No matter how you slice it though, its still a cut and paste job, and when I'm blogging, I try to avoid that.  :)

So, what I want to do here is extract the MSBuild and Exec tasks out of the DeployNumber1 and DeployNumber2 targets, and move them to their own target.  Now, there's a trick to doing this because MSBuild inherently keeps track of when a task has been run, and by default will only allow you to run a single target once within the same project.  You can use the Inputs and Outputs attributes on the Target element in some cases to force task to run again, but that doesn't necessarily apply here.

The best way I have found around this, is to use the MSBuild task and have the project call itself to execute the target, passing the database name and user name to run the database build steps below.

The below project shows the refactored project:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Default" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
    <Target Name="Default">
        <CallTarget Targets="DeployNumber1;DeployNumber2" />
    </Target>
 
    <Target Name="DeployNumber1">
        <MSBuild Projects="$(MSBuildProjectFile)" Properties="DBName=Database1;DBUser=UserForInstance1;"
                Targets="DeployDatabase" />
        <Message Text="Deploy 1 Done" />
    </Target>
 
    <Target Name="DeployNumber2">
        <MSBuild Projects="$(MSBuildProjectFile)" Properties="DBName=Database2;DBUser=UserForInstance2;"
                Targets="DeployDatabase" />
        <Message Text="Deploy 2 Done" />
    </Target>
 
    <Target Name="DeployDatabase">
        <MSBuild Projects="buildDatabase.proj" Properties="DBName=$(DBName)" />
        <Exec Command="sqlcmd.exe -S (local) -d $(DBName) -Q &quot;CREATE USER [$(DBUser)] FOR LOGIN [$(DBUser)]&quot; -E -b -I" />
        <Exec Command="sqlcmd.exe -S (local) -d $(DBName) -Q &quot;EXEC sp_addrolemember N'db_owner', N'$(DBUser)'&quot; -E -b -I" />
    </Target>
 
</Project>