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.
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:
- Confirms the code was built and released through an approved CI/CD process – the presence of a default “184.108.40.206” version indicates worrying “laptop” deployments *
- Confirms the production code matches the tested code (and hasn’t had any bonus unpredictability merged in)
- Tells you where to find the exact code revision in source control
- Tells you which build job created it
- 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:
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 “220.127.116.110bcd5309dacf3897fc41ba11dff56409b136db”).
These numbers are baked into the compiled code (inspect the properties of a DLL file).
Your default AssemblyInfo.cs entries look like this:
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
Attribute.GetCustomAttributes(Assembly.GetExecutingAssembly(), 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:
- Edit Project->Parameters
- 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:
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.