Thursday, May 31, 2007

Numbers, numbers everywhere - fixing VS2005's Assembly Numbering Limitations

Assembly numbering isn't something that a developer will generally bother too much about - so long as your references are up to date and the code works, what does it matter if the AssemblyVersion or AssemblyFileVersion are permanently locked at 1.0.0.0? Unfortunately, with the complex interaction of class-libraries in large projects, we need to be a bit more deliberate about things.

The two properties in question, AssemblyVersion and AssemblyFileVersion, are controlled through assembly attributes in the AssemblyInfo.cs file for a class library. You can find this in the Properties folder of a project.

When you create a new Class Library project, these properties will be set as shown in the following snippet from AssemblyInfo.cs.

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("1.0.0.*")]
[assembly: AssemblyFileVersion("1.0.0.*")]

Visual studio lets you "default" the Build and Revision numbers using an asterisk (*) in the property. HOWEVER, it ONLY works for the AssemblyVersion property, NOT the AssemblyFileVersion property. This means that whilst your AssemblyVersion can automatically increment, the AssemblyFileVersion cannot.

Fortunately, there's a workaround from Microsoft - an additional MSBuild task that can be added to a project file which causes the two properties to be updated on every build.

Of course there's a downside - setting this up for every project involves adding a line to the .csproj file directly. What follows is a walkthrough of the process.
  1. Install the AssemblyInfo task downloaded from the MSBuild Tasks website.
  2. Create a customised build target file according to the instructions.

    I created a copy in the installation directory, but you can put it wherever you want - just be sure to update the path in step 3 below.

    My customisation was configure the task so that the AssemblyFileVersion had fixed Major and Minor versions, but the Build number represented the day and month the build took place and the Revision number again autoincremented.

    The critical code from the targets file is this:


    <PropertyGroup>
    <AssemblyFileMajorVersion>1</AssemblyFileMajorVersion>
    <AssemblyFileMinorVersion>0</AssemblyFileMinorVersion>
    <AssemblyFileBuildNumber></AssemblyFileBuildNumber>
    <AssemblyFileRevision></AssemblyFileRevision>
    <AssemblyFileBuildNumberType>DateString</AssemblyFileBuildNumberType>
    <AssemblyFileBuildNumberFormat>00MMdd</AssemblyFileBuildNumberFormat>
    <AssemblyFileRevisionType>AutoIncrement</AssemblyFileRevisionType>
    <AssemblyFileRevisionFormat>00</AssemblyFileRevisionFormat>
    </PropertyGroup>


    You can, of course, configure the task to modify the AssemblyVersion in the same way.
  3. Add the following additional target import to each project file (*.csproj) just after the default Microsoft.CSharp.targets import.

    <Import Project="$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Custom.VersionNumber.Targets"/>

    This is the magic that instructs the compiler to run the AssemblyVersion task each time the project is build.
It's a small chore to update each project, but the result is a much more useful format for the two properties.

[assembly: AssemblyVersion("1.0.0.03")]
[assembly: AssemblyFileVersion("1.0.000214.02")]

The AssemblyVersion build number auto-increments, and the FileVersion includes the build date where before it was just a static value.

There's one slight wrinkle with this method of your project is under source control, however. The AssemblyVersion task it relies on being able to write changes into the AssemblyInfo.cs files just prior to compilation - so if your source control package write-protects files until you check them out, the process will error.

For TFS users, this post on the MSBuild Team Blog gives additional build targets to add to your project file that will check-out the relevant files, and then check them back in again afterwards. It's targetted at automated builds, but is easy enough to modify.

But the simplest method is simple to ensure that AssemblyInfo.cs is checked out before you build by using a pre-build script in each project. The following script works perfectly for TFS-controlled projects (note the quotes, tho', and the use of a relative path for the AssemblyInfo.cs file).
"$(DevEnvDir)\tf.exe" checkout ..\..\Properties\AssemblyInfo.cs

Of course, this won't check in the changed files, but that's sometimes a benefit!

The AssemblyVersion MSBuild Task allows fine-grained control of assembly and file version numbers - when this method is linked with that of a ProductInfo.cs solution file (on which I'll blog later), you gain real control over the meta-data embedded into your products.