Best practices/guidance for maintaining assembly version numbers

Versioning is something that I am very passionate about and have spent a long time trying to come up with an easy to use versioning system. From what you have already said in your question it is clear that you have understood one important point, the assembly version numbers are not synonymous with the product version. One is technically driven, and the other is driven by the business.

The following assumes that you use some form of source control and a build server. For context we use TeamCity and Subversion/Git. TeamCity is free for a small (10) number of projects and is a very good build server but there are others, some of which are completely free.

What a version number means

What a version means to one person may mean something different to another, the general structure is major, minor, macro, micro. The way I look at a version number is to break it down into two parts. The first half describes the main version (Major) and any key updates (Minor). The second half indicates when it was built and what the source code version was. Version numbers also mean different things depending on the context, is it an API, Web App, etc.

Major.Minor.Build.Revision

  • Revision This is the number taken from source control to identify what was actually built.
  • Build This is an ever increasing number that can be used to find a particular build on the build server. It is an important number because the build server may have built the same source twice with a different set of parameters. Using the build number in conjunction with the source number allows you to identify what was built and how.
  • Minor This should only change when there is a significant change to the public interface. For instance, if it is an API, would consuming code still be able to compile? This number should be reset to zero when the Major number changes.
  • Major indicates what version of the product you are on. For example the Major of all the VisualStudio 2008 assemblies is 9 and VisualStudio 2010 is 10.

The exception to the rule

There are always exceptions to the rule and you will have to adapt as you come across them. My original approach was based on using subversion but recently I have moved to Git. Source control like subversion and source safe that use a central repository have a number that can be used to identify a particular set of sources from a given time. This is not the case for a distributed source control such as Git. Because Git uses distributed repositories that are on each development machine there is no auto incrementing number that you can use, there is a hack which uses the number of check-ins but it is ugly. Because of this I have had to evolve my approach.

Major.Minor.Macro.Build

The revision number has now gone, build has shifted to where the revision used to be and Macro has been inserted. You can use the macro how you see fit but most of the time I leave it alone. Because we use TeamCity the information lost from the revision number can be found in the build, it does mean there is a two step process but we have not lost anything and is an acceptable compromise.

What to set

The first thing to understand is that the Assembly Version, File Version and Product Version do not have to match. I am not advocating having different sets of numbers but it makes life a lot easier when making small changes to an assembly that doesn't affect any public interfaces that you are not forced to recompile dependent assemblies. The way I deal with this is to only set the Major and Minor numbers in the Assembly Version but to set all the values in the File Version. For example:

  • 1.2.0.0 (AssemblyVersion)
  • 1.2.3.4 (FileVersion)

This gives you the ability to roll out hot fixes which will not break existing code because the assembly versions do not match but allow you to see the revision/build of an assembly by looking at its file version number. This is a common approach and can be seen on some open source assemblies when you look at the assembly details.

You as the Team lead would need to be responsible for incrementing the minor number when ever a breaking change is required. One solution to rolling out a required change to an interface but not breaking previous code is to mark the current one as obsolete and creating a new interface. It means that existing code is warned that the method is obsolete and could be removed at any time but doesn't require you to break everything immediately. You can then remove the obsolete method when everything has been migrated.

How to wire it together

You could do all the above manually but it would be very time consuming, the following is how we automate the process. Each step is runnable.

  • Remove the AssemblyVersion and AssemblyFileVersion attributes from all the project AssemblyInfo.cs files.
  • Create a common assembly info file (call it VersionInfo.cs) and add it as a linked item to all your projects.
  • Add AssemblyVersion and AssemblyFileVersion attributes to the version with values of "0.0.0.0".
  • Create an MsBuild project that builds your solution file.
  • Add in a task prior to the build that updates the VersionInfo.cs. There are a number of open source MsBuild libraries that include an AssemblyInfo task which can set the version number. Just set it to an arbitrary number and test.
  • Add a property group containing a property for each of the segments of the build number. This is where you set the major and minor. The build and revision number should be passed in as arguments.

With subversion:

<PropertyGroup>
    <Version-Major>0</Version-Major>
    <Version-Minor>0</Version-Minor>
    <Version-Build Condition=" '$(build_number)' == '' ">0</Version-Build>
    <Version-Build Condition=" '$(build_number)' != '' ">$(build_number)</Version-Build>
    <Version-Revision Condition=" '$(revision_number)' == '' ">0</Version-Revision>
    <Version-Revision Condition=" '$(revision_number)' != '' ">$(revision_number)</Version-Revision>
</PropertyGroup>

Hopefully I have been clear but there is a lot involved. Please ask any questions. I will use any feedback to put a more concise blog post together.

  • Version numbers in a compiled assembly
  • MSBuild Extension Pack
  • TeamCity

The [AssemblyVersion] is a very big deal in .NET. One philosophy, encouraged by Microsoft is that you let it auto-increment, forcing all projects that depend on the assembly to be recompiled. Works okayish if you use a build server. It is never the wrong thing to do but beware of people carrying swords.

The other one, more closely associated with its actual meaning is that the number is representative for the versioning of the public interface of the assembly. In other words, you only change it when you alter a public interface or class. Since only such a change requires clients of the assembly to be recompiled. This needs to be done manually though, the build system isn't smart enough to auto-detect such a change.

You can further extend this approach by only incrementing the version when the assembly was deployed on machines outside of your reach. This is the approach that Microsoft uses, their .NET assemblies version numbers very rarely change. Mostly because of the very considerable pain it causes on their customers.

So what Microsoft preaches is not what it practices. Its build process and versioning control is however unparalleled, they even have a dedicated software engineer that monitors the process. Didn't quite work out so well, the WaitHandle.WaitOne(int) overload in particular caused a fair amount of pain. Fixed in .NET 4.0 with a very different approach, but that's getting a bit beyond the scope.

It is rather up to you and your confidence in how well you can control the build process and the release cycles to make your own choice. Other than that, auto-incrementing the [AssemblyFileVersion] automatically is very appropriate. With however the inconvenience that this is not supported.


You could use the Build part of the version number for auto-increment.

[assembly: AssemblyVersion("1.0.*")]

In your environment a test version is a version that has a build version != 0. On release you increment the minor part and set the build part to 0, this is how you would identify released assemblies.

If you install your assemblies in the GAC your GAC gets flooded with lots of diffent versions over time, so keep that in mind. But if you use the dlls only locally, I think this is a good practice.


Adding to Bronumskis answer, I want to point out that after the Semantic Versioning 2.0 standard at semver.org, Major.Minor.Build.Revision would be illegal due to the rule that after increasing a number, all regular values to the right would have to be reset to zero.

A better way following the standard would be to use Major.Minor+Build.Revision. This is obivously not for use in AssemblyVersionAttribute, but a custom attribute or static class could be used instead.

Semver in TeamCity should be available using the Meta-runner Power Pack. For git with git-flow (especially in the .NET world), I found GitVersion to be helpful.