Versioning .Net Applications With Team City and Git/SVN/TFS

Some notes on setting up version numbering for .Net applications and having your build server manage everything (I’m using Team City). I’ll cover some differences between centralised (TFS, SVN) and decentralised (Git) version control. These notes are based on a couple of “single-branch, build-once-and-redeploy” continuous delivery pipelines I set up for projects in TFS and Git.

Why Version?

We’ve all seen software shipped from developer machines, hoped we shipped the code we actually tested, and had to troubleshoot live issues where all we know about the code is it’s “probably whatever Bob had on his laptop three weeks ago”. Want the easy life? Bake in a version number that:

  1. Confirms the code was built and released through an approved CI/CD process – the presence of a default “” version indicates worrying “laptop” deployments *
  2. Confirms the production code matches the tested code (and hasn’t had any bonus unpredictability merged in)
  3. Tells you where to find the exact code revision in source control
  4. Tells you which build job created it
  5. Gives you a changing version number to display to users that says “new stuff”

Display that version somewhere in your code and have it available on your build server.

* OK, you could cheat and fake a version number, but the aim is to make things so easy that doing things properly is the only option you consider.

What Version Number Format Should I Use?

This one is up to you. .Net projects allow a basic four-segment numbering system.

A recommended format is: “<major version>.<minor version>.<build number>.<revision>”.

Major/minor components are typically your “marketing version”, but you might want to use them to signal build time (eg. calendar date or sprint number). They probably won’t change often, and updates may be manually controlled. You definitely want to have build and revision/changeset numbers automatically applied so you can trace where the code came from. If your source control doesn’t use sequential version numbers (eg. Git) then you need to make a few adjustments, typically by using the revision checksum/snapshot instead of a sequential code revision.

Displaying the Version Number In Your Application

Your application should display the version number somewhere – on a help/about dialog, footer, via an “info” API call, etc.

Within your code, each project has an “AssemblyInfo.cs” file under “properties”. This can contain versioning attributes for:

  • AssemblyVersion
  • AssemblyFileVersion
  • AssemblyInformationalVersion

Version and file-version components consist of four numbers (you might be able to squeeze letters into the file version, but you’ll get a warning). The informational version allows more freedom, so you could hide a checksum, like a Git revision (yes, I really did ship an application with version number “”).

These numbers are baked into the compiled code (inspect the properties of a DLL file).

Your default AssemblyInfo.cs entries look like this:

[assembly: AssemblyVersion(“”)]
[assembly: AssemblyFileVersion(“”)]

You can manually edit these and rebuild, but much better to get your build process to apply them automatically. To display, just use:


For the AssemblyInformationalVersion, use

typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute[]

And then display the first item in the array.

If you’re using Git, the AssemblyInformationalVersion is the place to store your revision.

Build Pipelines for Continuous Delivery / Continuous Deployment

I’m assuming you have some sort of pipeline whereby code passes through an initial CI (build on code change) stage to Test (deployment) to Live (deployment). You can control the flow of this pipeline in a couple of different ways:

  • Single branch / “trunk-based” deployment – everyone commits from local workspace (or their own development branches) to a single main branch, and you build once (at the Continuous Integration stage) then redeploy the same package/artifact at subsequent Test/Live stages
  • Branch-based – you merge code from Dev to Test to Live branches, and rebuild at each stage before deploying

You can use either approach, but always “ship what you test”. Because centralised version control systems (eg. SVN and TFS) assign a new sequential revision number when you merge code to a different branch, it’s difficult to track code changes across branches, so I wouldn’t recommend using a branch-based approach for SVN or TFS unless your process involves full continuous deployment at every step and you trust to full automated test coverage. With Git’s decentralised model, you have a checksum built into to the revision, so you can tell whether the merge introduced any changes, and a branch-based continuous delivery approach becomes viable.


Versioning in Team City

Each build in Team City is assigned a build number. You edit this in the configuration’s General Settings panel, just enter a value for “Build number format”. Typically this is built from a number of properties, the default being the “%build.counter%” (Team City’s internal count of builds for that configuration, also editable on the General Settings panel).

While the build counter is managed by Team City and the revision tracks back to source control, other build numbers can be applied. So to manage “marketing” versions for your product:

  1. Edit Project->Parameters
  2. Create two “configuration” parameters – “Project.Major.version” and “Project.Minor.version”

You could set up parameters for an individual build if you want, but project-wide parameters make sense. These values are now available to use in components of other build numbers as “%Project.Major.version%” and “%Project.Minor.version%”.

Some clarification – there’s a few different build numbers you’ll have to manage here:

  • The Team City build counter – increments each time Team City runs a specific build configuration job eg. “build code for project X” (this is a single number)
  • The Team City build number – applied to each build run, displayed on Team City dashboard and build reports (this might be a four-digit version number)
  • The version number(s) baked into the code at build time

Team City makes setting version numbers really easy, using the “Assembly Info Patcher” build feature – use this for every build configuration that is actually building code (CI, or any branch-based “re-build and re-deploy” stage). Just edit a build configuration and go to Build Features->Assembly Info Patcher. Set the required “Assembly version”, “Assembly file version” and “Assembly informational version” values to edit the
corresponding attributes in the AssemblyInfo.cs files.

Build Number Format (TFS)

For the initial CI build configuration use a build number format of


I just set this as the version and file version in the assembly info patcher dialog, and ignore the informational version.

If you’re taking the single-branch/build-once-and-redeploy approach, then for a chained/dependent build configuration (eg. Deployments to Test/Live) set the build number of subsequent builds in the chain to be

You can use this approach to assign the same build number to the same code built/deployed by different build configurations – I do this to track a release candidate from the initial CI stage (build and package) through the various Test and Live deployment pipeline stages.

Build Number Format (Git)

For Git, I set the build number format to


This means I only get 3-digit version numbers displayed on Team City, and no direct reference to the code.

I then set the assembly info patch values to be (on Build Features->Assembly Info Patcher)

Version and file version:


Informational version:


Passing Team City Version to Build Scripts

A quick note: Team City sets the current build number (the full 4-digit one) in a “BUILD_NUMBER” environment variable. So you could have an MsBuild script in your project use this if you need it (eg. to name files, or append to release notes and emails).

Example MsBuild usage:

<Message Text="Build number=$(BUILD_NUMBER)"/>



So you can use your build server to generate meaningful version numbers, track the same code as it works through the various chained builds of your build pipeline from initial commit to production deployment, and bake the appropriate version into your code. With a version number available, you should have confidence that the production release is known and tested software, and not some mystery release you can’t recreate locally.


MsBuild Version Gotchas – Better Building with MsBuild scripts and Build Servers

I recently ran into issues when upgrading an old codebase from the original setup (Visual Studio 2013 and the MsBuild tools v4.0) to use Visual Studio 2015, building through Team City on a new machine that only had VS2015. The MsBuild scripts I had been using for building (mostly creating the deployment package) and actual deployment (calling MsDeploy) needed a few tweaks. Mostly, I had taken and adapted a boilerplate MsBuild script from an earlier project, left in some stupid hacks that worked well until they didn’t, and found my scripts needed a few tweaks to work with the current tools.

My build process was to use Team City to compile, package, and later deploy (via MsDeploy) a web app to Azure, using MsBuild scripts to manage the actual solution build/package/deploy specifics.

For the purposes of this post, Team City happens to be my build server of choice. If the detail of your build process is stored and versioned with the code (and not the build server configs) then switching build servers should be easy.

So this post is some note-to-future-self gotchas for the next time a tools upgrade breaks the build…

MsBuild.exe path and version

First issue was to check the location of the MsBuild.exe itself. As this wasn’t already in my path, I had been specifying it explicitly as a command prompt setup for locally debugging build scripts, using a “commandPrompt.bat” with the following line

@%comspec% /k "SET PATH=C:\Windows\Microsoft.NET\Framework\v4.0.30319;

So first issue is to fix the path and use the latest version – “C:\Program Files (x86)\MSBuild\14.0\Bin”.

Setting the local path to MsBuild isn’t strictly required – normally I’d build locally through Visual Studio, but I like being able to test the build scripts locally without having to push them to the build server, and wrapping the build specifics in an MsBuild script lets me do this. The important thing is that I want Visual Studio and Team City build to compile the code with the same MsBuild engine so there’s no surprises.

Whenever I run the MsBuild script eg. with a

msbuild WebPackage.msbuild /t:BuildPackage

I get the MsBuild engine version reported, but to help debug my scripts, I tend to put some kind of “Info” target in to report paths and settings, eg:

<Target Name="Info">
 <Message Text="MsBuildExtensionsPath=$(MsBuildExtensionsPath)" />
 <Message Text="MSBuildToolsVersion=$(MSBuildToolsVersion)"/>
 <Message Text="VisualStudioVersion=$(VisualStudioVersion)"/>
 <Message Text="VSToolsPath=$(VSToolsPath)"/>

Now I can run:

msbuild WebPackage.msbuild /t:Info

If the latest MsBuild is on the path, I should get latest version reported (v14.0).

Also, in my .csproj file I found references in a PropertyGroup to “VisualStudioVersion” and “VSToolsPath” settings.

 <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
 <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v(VisualStudioVersion)</VSToolsPath>

Your build scripts shouldn’t really be tying themselves to specific tools versions, better to set these values and pass them in to any running MsBuild process.

MsBuild Scripts Calling MsBuild

Noticed another problem – I had my MsBuild script calling the MsBuild.exe directly:

<Exec Command="$(MSBuildExe) $(SolutionName).sln /t:clean"/>

…and because I couldn’t guarantee MsBuild.exe in the path, I cheated and had my MsBuild script setting the MsBuild.exe path to a default if it wasn’t overridden:

<MSBuildExe Condition=" '$(MSBuildExe)'=='' ">C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe</MSBuildExe>

So checking the output, I was using the old MsBuild engine version. Oops. I could probably have allowed the .exe location to be specified either on the path, as an argument to the script, or as a default in the script, but that’s a hassle. Luckily, to free myself from nasty tools path dependencies, I can rewrite my MsBuild calls as:

<MSBuild Projects="$(SolutionName).sln" Targets="clean"/>

The general format is:

<MSBuild Projects="filename" Targets="target" Properties="prop1=val1;prop2=val2"/>

This should mean that whatever MsBuild engine you use to kick off the script is the one that gets used throughout. I have had issues with a few quotes around property values (solution: don’t quote property values), but it should be possible to express all MsBuild calls using the MsBuild task.

Importing MsBuild Tasks

Ok, for some reason, probably relating to some hacky web.config transforming I had done on previous projects (I have done terrible things to get applications to deploy and workaround the “configure-build-deploy” workflow of web.configs), I had the following using statement in my MsBuild script:

<UsingTask TaskName="TransformXml" AssemblyFile="C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll"/>

This was actually referencing non-existent DLL

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll

Which should be

C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll

There’s another issue here – this will almost certainly require Visual Studio installed on the build server, or a cut down install eg.

Turns out, I actually no longer needed to use this task for building this project and I can just omit this line. But there will be times when your build script needs access to those extra tasks. If you can, try to avoid your build script knowing exactly where on the build server to look for stuff.

Team City Settings

So finally, I had to go into my Team City build configuration for all my build steps that used MsBuild and upgrade the MsBuild tools versions to latest. And check you’re using the same version throughout the build (I ran into problems compiling and packaging the same web app with different MsBuild versions in different Team City build steps).


Things to watch for:

  • Wrong MsBuild version on path – to debug, create an “Info” target in your build script and echo property values to console / build server log
  • Specifying the MsBuild version in your script – use the “MsBuild” task instead of direct MsBuild.exe calls to prevent specifying the .exe location on the path or in your script
  • References to a specific Visual Studio / tools version that may not be installed – eg. common MsBuild targets
  • Copying-and-pasting a previous build script and not cleaning out the unused stuff when you find a better way of doing things

Also – limit the use of MsBuild, try keep it simple, use it for building, packaging, including/excluding/copying files to be deployed. Handle deployment and other complex tasks through Powershell.