Versioning .NET Core in Visual Studio Team Services(8 min read)

I have always found it useful for applications to display their build version, and for libraries to have the build version in their properties. Relying on properties like the date (or file size) is always a bit risky.

.NET Core has embraced Semantic Versioning and at first glance appears to have a new way to specify version numbers.

It doesn't quite work to my full satisfaction, but luckily the older methods still work, so a basic GitVersion task in your build pipeline is pretty much all you need to get things working.

Setting the version number in .NET Core

SemVer versioning:

The version attribute in project.json starts with up to three or four dotted numbers, then a dash and a pre-release identifier (any number of dot separated parts containing alpha, digits, and dashes).

Digits up to the first dash are formatted into three (or four, if there are four) for Product Version, and into four digits for the other values (which are structures with four values).

This is not quite SemVer, as it should only support 3 parts (the fourth is for backwards compatibility with earlier .NET). Usually the fourth value should just be set to zero (by only using three).

// project.json
{
    "version": "1.2-3.4-5.6-7-8"
}

Once you have the value set, as well as being visible in library properties, it is also useful to output (e.g. from a console app, or displayed on a web page).

The ApplicationVersion is from the Microsoft.Extensions.PlatformAbstractions package, and shows the same value as AssemblyVersion. The other two values, FileVersion and ProductVersion (aka InformationalVersion) are from the System.Diagnostics.FileVersionInfo package.

Generally you want to display the ProductVersion.

var applicationVersion = PlatformServices.Default.Application.ApplicationVersion;
var assembly = typeof(Program).GetTypeInfo().Assembly;
var engineFileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
var machineName = Environment.MachineName;
Console.WriteLine("ApplicationVersion = '{0}'", applicationVersion);
Console.WriteLine("AssemblyVersion = '{0}'", assembly.GetName().Version);
Console.WriteLine("FileVersion = '{0}'", engineFileVersionInfo.FileVersion);
Console.WriteLine("ProductVersion = '{0}'", engineFileVersionInfo.ProductVersion);

Output, note that when given only two parts, a third 0 is added to ProductVersion, and a fourth 0 to the others:

ApplicationVersion = '1.2.0.0'
AssemblyVersion = '1.2.0.0'
FileVersion = '1.2.0.0'
ProductVersion = '1.2.0-3.4-5.6-7-8'

Alpha okay after the dash:

Using more than four numeric parts before the dash, or other alpha characters, will cause a formatting error, although they are okay after the dash.

// project.json
{
    "version": "1.2.3.4-alpha.2"
}

Output, note that ProductVersion contains the full four digits (not SemVer compliant):

ApplicationVersion = '1.2.3.4'
AssemblyVersion = '1.2.3.4'
FileVersion = '1.2.3.4'
ProductVersion = '1.2.3.4-alpha.2'

Special replacement (after the dash):

A "-*" at the end of the version, is replaced during build (or left off entirely if not specified). This suffix works is okay even if there are other dashes and text before, i.e. the pre-release can already be partially specified.

// project.json
{
    "version": "1.2-3.4-5.6-7-*"
}

Build (or Package):

dotnet build --version-suffix "99.test"

Output, noting that --version-suffix only ever affects ProductVersion:

ApplicationVersion = '1.2.0.0'
AssemblyVersion = '1.2.0.0'
FileVersion = '1.2.0.0'
ProductVersion = '1.2.0-3.4-5.6-7-99.test'

SemVer build metadata is ignored:

A "+" character, indicating build metadata, is valid, but it and anything after it is ignored and not output. Note that the truncation also occurs if passed in from --version-suffix.

// project.json
{
    "version": "1.2.3-alpha.4+56.Branch.master",
}

Output, loses the build metadata:

ApplicationVersion = '1.2.3.0'
AssemblyVersion = '1.2.3.0'
FileVersion = '1.2.3.0'
ProductVersion = '1.2.3-alpha.4'

AssemblyInfo.cs overrides:

Setting AssemblyVersion in code overrides the value. Note that ApplicationVersion still gives the same value as the overridden AssemblyVersion attribute.

// project.json
{
    "version": "1.2.3-*",
}

Overridden in code:

// AssemblyInfo.cs
[assembly: AssemblyVersion("1.4.0.0")]

Build:

dotnet build --version-suffix "beta.2+99"

Output, AssemblyVersion, and ApplicationVersion, are overriden (also shows the build metadata from --version-suffix is dropped):

ApplicationVersion = '1.4.0.0'
AssemblyVersion = '1.4.0.0'
FileVersion = '1.2.3.0'
ProductVersion = '1.2.3-beta.2'

Finally getting the build metadata in the output:

The AssemblyFileVersion and AssemblyInformationalVersion can also be overriden separately.  The AssemblyInformationalVersion, which is output as the ProductVersion, supports any text (the others must be four numbers), and is the one we want to use.

// project.json
{
    "version": "1.2.3",
}

And, in code:

// AssemblyInfo.cs
[assembly: AssemblyFileVersion("1.5.6.0")]
[assembly: AssemblyInformationalVersion("1.7.8.0-beta+9")]

Output, finally we see the build information in the output:

ApplicationVersion = '1.2.3.0'
AssemblyVersion = '1.2.3.0'
FileVersion = '1.5.6.0'
ProductVersion = '1.7.8.0-beta+9'

Evaluation of the new versioning (project.json)

The new version property in project.json doesn't do everything I want it to:

  • It doesn't support the build identifiers from SemVer -- it puts the pre-release information into ProductVersion (aka AssemblyInformationalVersion), but drops off everything after the "+'.
  • It also doesn't integrate with your source control (e.g. Git), and has to be manually incremented.

Work around (not recommended):

If you insist on using project.json, and want a basic overide, use the --version-suffix additional argument in your build/package step.

Use an incrementing value (e.g. Build Number or Build ID in VSTS) and pass it to the build or package commands as --version-suffix "$(Build.BuildId)"

This would at least get unique values injected into the ProductVersion for each build, although as pre-release identifiers, rather than build metadata. You would still have to update major/minor/patch manually, and consistently, across your projects.

Recommendations

Recommendation:

If you are using Git (and you should be), then in VSTS simply add the GitVersion task, from the Marketplace catalog, at the start of your build (and enable the Update AssemblyInfo files option in the task).

That's pretty much it (but see below for corner cases).

More detail:

GitVersion will auto-generate build number variables based on the branch, tag, and other details in Git. It is quite good at working out what you are building (main release, alpha/beta, explicit tags, etc), and then adds a build increment based on the number of commits since.

In the VSTS Build, set the Build number format to "$(GITVERSION_FullSemVer)", which will display the version number calculated from Git (including the number of commits, but not the detailed hash) for each build.

e.g. With no SemVer tags in you Git repo, it will give you something like "0.1.0+22"

Then set the Release name format to "$(Build.BuildNumber) [$(Build.SourceBranch)]", which has the same number, but also shows the branch name, and is still short enough to see on the Overview page of different environments.

To get the version numbers into your application, make sure you have checked the Update AssemblyInfo files option in the GitVersion task.

This will create AssemblyInfo.cs files if needed, and put in AssemblyVersion (major.minor), AssemblyFileVersion (major.minor.patch), and AssemblyInformationalVersion (the full SemVer version, including build details).

As these override what is in project.json, it can be ignored and left at "1.0.0-*"

Web applications:

In testing I found that just ticking the Update AssemblyInfo files option didn't work for web applications. For other project types (libraries and console), the version numbers are inserted (I guess it creates AssemblyInfo.cs), but not for web applications.

To make it work, I had to manually add an AssemblyInfo.cs file to the project (right click on project, Add -> New Item..., select Assembly Information File from the Code category), and then put in the three required attributes (AssemblyVersion, AssemblyFileVersion, and AssemblyInformationalVersion) with default values -- once the file existed in the ASP.NET Core project, then the Git Version task correctly updated it and the version numbers were applied to the web application.

Dependency version references:

In theory this should probably be a problem, where you have projects dependent on each other, except it seemed to work fine and everything works despite the mismatched version numbers.

e.g. Checking the .deps.json file in the deployed web application, it has a dependency on a library from the same VS solution, with the version specified as per the project.json version:

"dependencies": {
...
    "NBitcoin.PaymentServer/1.0.0": {
        "dependencies": {
            "Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
            "NBitcoin": "3.0.2.4",
            "NETStandard.Library": "1.6.1"
         },
    "runtime": {
        "NBitcoin.PaymentServer.dll": {}
    },
...

Now, this says it is dependent on version 1.0.0, however it points to the DLL in the current folder and everything works fine, despite the fact that the actual DLL has a different version (check the version of the DLL shows it is 0.1.0).

So, while dependencies are important at development time, they don't appear to actually be checked at runtime (i.e. no strong naming), so, "it just works".

If you really want to have the correct metadata then the solution would be a simple PowerShell script immediately after Git Version, that goes through and updates project.json files.

It needs to update the version, to make sure things compile, but most importantly update where there are dependencies, to fix the metadata.

To determine the correct dependencies to update probably requires to correctly parse the project.json, although if you use the special format with "-*" at the end you might be able to get away with a regular expression (replace all "1.0.0-*" with the values from Git Version).

Having the correct dependency versions is important if you are going to be releasing your work as a component that others will consume, but if the application is entirely internal then it will work without setting these values.

 

Leave a Reply

Your email address will not be published. Required fields are marked *