Simple MSBuild Configuration: Updating Assemblies With A Version Number

When distributing a library you often run up against versioning problems, once facet of which is simply determining which version of that library your client is running.  Of course, each project in your solution has an AssemblyInfo.cs file which provides, among other things, the ability to set the Assembly name and version number.  Unfortunately, setting the assembly version here would require not only changing the version manually for each build (depending on your schedule), but keeping it in sync across all projects. 

There are many ways to solve this versioning problem, and in this blog post I’m going to try to explain what I think is the easiest and most flexible solution.  I will walk you through using MSBuild to create a simple build script, and I’ll even show how to (optionally) integrate with a Team City build server.  All of the code from this post can be found at https://github.com/srkirkland/BuildVersion.

Create CommonAssemblyInfo.cs

The first step is to create a common location for the repeated assembly info that is spread across all of your projects.  Create a new solution-level file (I usually create a Build/ folder in the solution root, but anywhere reachable by all your projects will do) called CommonAssemblyInfo.cs.  In here you can put any information common to all your assemblies, including the version number.  An example CommonAssemblyInfo.cs is as follows:

using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
 
[assembly: AssemblyCompany("University of California, Davis")]
[assembly: AssemblyProduct("BuildVersionTest")]
[assembly: AssemblyCopyright("Scott Kirkland & UC Regents")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyTrademark("")]
 
[assembly: ComVisible(false)]
 
[assembly: AssemblyVersion("1.2.3.4")] //Will be replaced
 
[assembly: NeutralResourcesLanguage("en-US")]

 

Cleanup AssemblyInfo.cs & Link CommonAssemblyInfo.cs

For each of your projects, you’ll want to clean up your assembly info to contain only information that is unique to that assembly – everything else will go in the CommonAssemblyInfo.cs file.  For most of my projects, that just means setting the AssemblyTitle, though you may feel AssemblyDescription is warranted.  An example AssemblyInfo.cs file is as follows:

using System.Reflection;
 
[assembly: AssemblyTitle("BuildVersionTest")]

Next, you need to “link” the CommonAssemblyinfo.cs file into your projects right beside your newly lean AssemblyInfo.cs file.  To do this, right click on your project and choose Add | Existing Item from the context menu.  Navigate to your CommonAssemblyinfo.cs file but instead of clicking Add, click the little down-arrow next to add and choose “Add as Link.”  You should see a little link graphic similar to this:

image

We’ve actually reduced complexity a lot already, because if you build all of your assemblies will have the same common info, including the product name and our static (fake) assembly version.  Let’s take this one step further and introduce a build script.

Create an MSBuild file

What we want from the build script (for now) is basically just to have the common assembly version number changed via a parameter (eventually to be passed in by the build server) and then for the project to build.  Also we’d like to have a flexibility to define what build configuration to use (debug, release, etc).

In order to find/replace the version number, we are going to use a Regular Expression to find and replace the text within your CommonAssemblyInfo.cs file.  There are many other ways to do this using community build task add-ins, but since we want to keep it simple let’s just define the Regular Expression task manually in a new file, Build.tasks (this example taken from the NuGet build.tasks file).

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Go" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="RegexTransform" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <ParameterGroup>
            <Items ParameterType="Microsoft.Build.Framework.ITaskItem[]" />
        </ParameterGroup>
        <Task>
            <Using Namespace="System.IO" />
            <Using Namespace="System.Text.RegularExpressions" />
            <Using Namespace="Microsoft.Build.Framework" />
            <Code Type="Fragment" Language="cs">
                <![CDATA[
            foreach(ITaskItem item in Items) {
              string fileName = item.GetMetadata("FullPath");
              string find = item.GetMetadata("Find");
              string replaceWith = item.GetMetadata("ReplaceWith");
              
              if(!File.Exists(fileName)) {
                Log.LogError(null, null, null, null, 0, 0, 0, 0, String.Format("Could not find version file: {0}", fileName), new object[0]);
              }
              string content = File.ReadAllText(fileName);
              File.WriteAllText(
                fileName,
                Regex.Replace(
                  content,
                  find,
                  replaceWith
                )
              );
            }
          ]]>
            </Code>
        </Task>
    </UsingTask>
</Project>

If you glance at the code, you’ll see it’s really just going a Regex.Replace() on a given file, which is exactly what we need.

Now we are ready to write our build file, called (by convention) Build.proj.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Go" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="$(MSBuildProjectDirectory)\Build.tasks" />
    <PropertyGroup>
        <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
        <SolutionRoot>$(MSBuildProjectDirectory)</SolutionRoot>
    </PropertyGroup>
 
    <ItemGroup>
        <RegexTransform Include="$(SolutionRoot)\CommonAssemblyInfo.cs">
            <Find>(?&lt;major&gt;\d+)\.(?&lt;minor&gt;\d+)\.\d+\.(?&lt;revision&gt;\d+)</Find>
            <ReplaceWith>$(BUILD_NUMBER)</ReplaceWith>
        </RegexTransform>
    </ItemGroup>
 
    <Target Name="Go" DependsOnTargets="UpdateAssemblyVersion; Build">
    </Target>
 
    <Target Name="UpdateAssemblyVersion" Condition="'$(BUILD_NUMBER)' != ''">
        <RegexTransform Items="@(RegexTransform)" />
    </Target>
 
    <Target Name="Build">
        <MSBuild Projects="$(SolutionRoot)\BuildVersionTest.sln" Targets="Build" />
    </Target>
 
</Project>

Reviewing this MSBuild file, we see that by default the “Go” target will be called, which in turn depends on “UpdateAssemblyVersion” and then “Build.”  We go ahead and import the Bulid.tasks file and then setup some handy properties for setting the build configuration and solution root (in this case, my build files are in the solution root, but we might want to create a Build/ directory later).  The rest of the file flows logically, we setup the RegexTransform to match version numbers such as <major>.<minor>.1.<revision> (1.2.3.4 in our example) and replace it with a $(BUILD_NUMBER) parameter which will be supplied externally.  The first target, “UpdateAssemblyVersion” just runs the RegexTransform, and the second target, “Build” just runs the default MSBuild on our solution.

Testing the MSBuild file locally

Now we have a build file which can replace assembly version numbers and build, so let’s setup a quick batch file to be able to build locally.  To do this you simply create a file called Build.cmd and have it call MSBuild on your Build.proj file.  I’ve added a bit more flexibility so you can specify build configuration and version number, which makes your Build.cmd look as follows:

set config=%1
if "%config%" == "" (
   set config=debug
)
set version=%2
if "%version%" == "" (
   set version=2.3.4.5
)
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\msbuild Build.proj /p:Configuration="%config%" /p:build_number="%version%"

Now if you click on the Build.cmd file, you will get a default debug build using the version 2.3.4.5.  Let’s run it in a command window with the parameters set for a release build version 2.0.1.453.

image

image 

Excellent!  We can now run one simple command and govern the build configuration and version number of our entire solution.  Each DLL produced will have the same version number, making determining which version of a library you are running very simple and accurate.

Configure the build server (TeamCity)

Of course you are not really going to want to run a build command manually every time, and typing in incrementing version numbers will also not be ideal.  A good solution is to have a computer (or set of computers) act as a build server and build your code for you, providing you a consistent environment, excellent reporting, and much more.  One of the most popular Build Servers is JetBrains’ TeamCity, and this last section will show you the few configuration parameters to use when setting up a build using your MSBuild file created earlier.  If you are using a different build server, the same principals should apply.

First, when setting up the project you want to specify the “Build Number Format,” often given in the form <major>.<minor>.<revision>.<build>.  In this case you will set major/minor manually, and optionally revision (or you can use your VCS revision number with %build.vcs.number%), and then build using the {0} wildcard.  Thus your build number format might look like this: 2.0.1.{0}.  During each build, this value will be created and passed into the $BUILD_NUMBER variable of our Build.proj file, which then uses it to decorate your assemblies with the proper version.

After setting up the build number, you must choose MSBuild as the Build Runner, then provide a path to your build file (Build.proj).  After specifying your MSBuild Version (equivalent to your .NET Framework Version), you have the option to specify targets (the default being “Go”) and additional MSBuild parameters.  The one parameter that is often useful is manually setting the configuration property (/p:Configuration="Release") if you want something other than the default (which is Debug in our example). 

Your resulting configuration will look something like this:

[Under General Settings]

image

[Build Runner Settings]

image 

Now every time your build is run, a newly incremented build version number will be generated and passed to MSBuild, which will then version your assemblies and build your solution.

 

A Quick Review

Our goal was to version our output assemblies in an automated way, and we accomplished it by performing a few quick steps:

  1. Move the common assembly information, including version, into a linked CommonAssemblyInfo.cs file
  2. Create a simple MSBuild script to replace the common assembly version number and build your solution
  3. Direct your build server to use the created MSBuild script

That’s really all there is to it.  You can find all of the code from this post at https://github.com/srkirkland/BuildVersion.

Enjoy!

3 Comments

Comments have been disabled for this content.