NuGet Versioning Hell

...
  • By Ivan Gavryliuk
  • In Architecture  |  Version Control  |  .NET
  • Posted 23/03/2017

In this post I'd like to point out why proper versioning of your .NET assemblies is important and how easy it is to get into NuGet versioning hell. First, a bit of a background how .NET versioning works.

Assembly Version Markers

If you have ever looked into AssemblyInfo.cs you've noticed those two tags:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.1.3.0")]

or, if you are using the new .NET Core project model with msbuild the project structure is slightly different, i.e. the version information is contained within the PropertyGroup element:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <AssemblyVersion>1.0.0.0</AssemblyVersion>
    <FileVersion>1.1.3.0</FileVersion>
    <Version>1.1.3.0</Version>
  </PropertyGroup>
</Project>

These are the only three elements you need to describe a proper version, but follow me below.

I'm not going to go into details on how project.json works as this is slightly outdated and was a horrible project system not compatible with existing .NET world (this is just my opinion of course).

From what I've seen everyone just ignores those leaving the defaults, use uses in a terribly wrong way. And that's absolutely fine if you are writing an end-user application, you won't get any kind of problems. It only matters if you are a library developer.

In essense, there are three markers which you should care about:

Img00

Where conflicts begin

Img01

Let's say we have an application v1.2.3 (actually application version doesn't play any role here) which references a library called common v1.0.1. It also references a Library v1.1.0, however this library depends on common v1.0.2. Once you made those references and start your application it will crash horribly with an error similar tho this:

System.IO.FileLoadException: Could not load file or assembly 'common, Version=1.0.1.0, Culture=neutral, PublicKeyToken=8dea52f811e261ef' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
File name: 'common, Version=1.0.2.0, Culture=neutral, PublicKeyToken=8dea52f811e261ef'
blah-blah-blah

One of the ways to solve this (if you own the library code) is to update the dependency to the same version as your application uses. This will break other applications using the libarary in the same way though. Or update your application to reference v1.0.2, although the next update of the library will crash your app again.

How did this happen?

Let's go back to our version markers and analyze what they are for:

  • AssemblyVersion. This is the most important attribute and is the one .NET Runtime uses to resolve the dependencies. Every assembly's manifest will contain sort of a contract which tells it which dependant's AssemblyVersion to use. If dependency's version doesn't match you will get this error.
  • FileVersion. This attribute is not used by .NET Runtime at all. It is a part of assembly's manifest, but only contained within the assembly which declares it. Your application has no use of it.
  • PackageVersion. Only used in .NET Core msbuild project to specify which version the target NuGet package will get stamped with. This is because the new msbuild can actually produce nugets out of the box.

The values of all of these can be completely different and don't have to match.

You've probably guessed that the developers of the libraries used AssemblyVersion to stamp their libraries with actual versions like 1.0.2, 1.0.3 etc. Hence the runtime conflict.

How to avoid this

When you are developing a library the simple rule to follow is to use AssemblyVersion only to mark a Major version of your product. This simply means that when it doesn't change, any rolling upgrades to the library don't break the functionality i.e. fully backward compatible with previous versions. This way you tell the runtime to use the same version, even if actual version of your library changes. Only change AssemblyVersion when you are releasing major upgrades which require breaking changes to the users's code.

Always mark the library's FileVersion with the actual version of your library, so it at least gives a hint to a user what they are on. I would strongly recomment stamping PackageVersion with the same marker so achieve some consistencly. I can't think of a case at the moment when they should be different, other than to completely confuse your users.

Now if we follow those conventions you'll get something like this (av - AssemblyVersion, fv - FileVersion):

Img02

common can be upgraded as many times as required, but as long as AssemblyVersion stays the same (1.0.0) all the dependencies are happy.

Hope you learned something new today.


Thanks for reading. If you would like to follow up with future posts please subscribe to my rss feed and/or follow me on twitter.