Versioning of assemblies in .NET can be a confusing prospect given that there are currently at least three ways to specify a version for your assembly.
Here are the three main version-related assembly attributes:
// Assembly mscorlib, Version 188.8.131.52
By convention, the four parts of the version are referred to as the Major Version, Minor Version, Build, and Revision.
Typically you’ll manually set the Major and Minor AssemblyFileVersion to reflect the version of the assembly, then increment the Build and/or Revision every time your build system compiles the assembly. The AssemblyFileVersion should allow you to uniquely identify a build of the assembly, so that you can use it as a starting point for debugging any problems.
On my current project we have the build server encode the changelist number from our source control repository into the Build and Revision parts of the AssemblyFileVersion. This allows us to map directly from an assembly to its source code, for any assembly generated by the build server (without having to use labels or branches in source control, or manually keeping any records of released versions).
This version number is stored in the Win32 version resource and can be seen when viewing the Windows Explorer property pages for the assembly.
The CLR does not care about nor examine the AssemblyFileVersion.
The AssemblyInformationalVersion is intended to allow coherent versioning of the entire product, which may consist of many assemblies that are independently versioned, perhaps with differing versioning policies, and potentially developed by disparate teams.
“For example, version 2.0 of a product might contain several assemblies; one of these assemblies is marked as version 1.0 since it’s a new assembly that didn’t ship in version 1.0 of the same product. Typically, you set the major and minor parts of this version number to represent the public version of your product. Then you increment the build and revision parts each time you package a complete product with all its assemblies.”
— Jeffrey Richter, CLR via C# (Second Edition) p. 57
The CLR does not care about nor examine the AssemblyInformationalVersion.
The AssemblyVersion is used by the CLR to bind to strongly named assemblies. It is stored in the AssemblyDef manifest metadata table of the built assembly, and in the AssemblyRef table of any assembly that references it.
This is very important, because it means that when you reference a strongly named assembly, you are tightly bound to a specific AssemblyVersion of that assembly. The entire AssemblyVersion must be an exact match for the binding to succeed. For example, if you reference version 184.108.40.206 of a strongly named assembly at build-time, but only version 220.127.116.11 of that assembly is available at runtime, binding will fail! (You will then have to work around this using Assembly Binding Redirection.)
There is a little confusion around whether the entire AssemblyVersion has to be an exact match in order for an assembly to be loaded. Some people are under the false belief that only the Major and Minor parts of the AssemblyVersion have to match in order for binding to succeed. This is a sensible assumption, however it is ultimately incorrect (as of .NET 3.5), and it’s trivial to verify this for your version of the CLR. Just execute this sample code.
On my machine the second assembly load fails, and the last two lines of the fusion log make it perfectly clear why:
.NET Framework Version: 2.0.50727.3521
Attempting to load assembly: Rhino.Mocks, Version=18.104.22.1687, Culture=neutral, PublicKeyToken=0b3305902db7183f
Successfully loaded assembly: Rhino.Mocks, Version=22.214.171.1247, Culture=neutral, PublicKeyToken=0b3305902db7183f
Attempting to load assembly: Rhino.Mocks, Version=126.96.36.1996, Culture=neutral, PublicKeyToken=0b3305902db7183f
Assembly binding for
System.IO.FileLoadException: Could not load file or assembly 'Rhino.Mocks, Version=188.8.131.526, Culture=neutral,
PublicKeyToken=0b3305902db7183f' or one of its dependencies. The located assembly's manifest definition
does not match the assembly reference. (Exception from HRESULT: 0x80131040)
File name: 'Rhino.Mocks, Version=184.108.40.2066, Culture=neutral, PublicKeyToken=0b3305902db7183f'
=== Pre-bind state information ===
LOG: User = Phoenix\Dani
LOG: DisplayName = Rhino.Mocks, Version=220.127.116.116, Culture=neutral, PublicKeyToken=0b3305902db7183f
LOG: Appbase = [...]
LOG: Initial PrivatePath = NULL
Calling assembly : AssemblyBinding, Version=18.104.22.168, Culture=neutral, PublicKeyToken=null.
LOG: This bind starts in default load context.
LOG: No application configuration file found.
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v2.0.50727\config\machine.config.
LOG: Post-policy reference: Rhino.Mocks, Version=22.214.171.1246, Culture=neutral, PublicKeyToken=0b3305902db7183f
LOG: Attempting download of new URL [...].
WRN: Comparing the assembly name resulted in the mismatch: Revision Number
ERR: Failed to complete setup of assembly (hr = 0x80131040). Probing terminated.
I think the source of this confusion is probably because Microsoft originally intended to be a little more lenient on this strict matching of the full AssemblyVersion, by matching only on the Major and Minor version parts:
“When loading an assembly, the CLR will automatically find the latest installed servicing version that matches the major/minor version of the assembly being requested.”
— Jeffrey Richter, CLR via C# (Second Edition) p. 56
This was the behaviour in Beta 1 of the 1.0 CLR, however this feature was removed before the 1.0 release, and hasn’t managed to re-surface in .NET 2.0:
“Note: I have just described how you should think of version numbers. Unfortunately, the CLR doesn’t treat version numbers this way. [In .NET 2.0], the CLR treats a version number as an opaque value, and if an assembly depends on version 126.96.36.199 of another assembly, the CLR tries to load version 188.8.131.52 only (unless a binding redirection is in place). However, Microsoft has plans to change the CLR’s loader in a future version so that it loads the latest build/revision for a given major/minor version of an assembly. For example, on a future version of the CLR, if the loader is trying to find version 184.108.40.206 of an assembly and version 220.127.116.11 exists, the loader with automatically pick up the latest servicing version. This will be a very welcome change to the CLR’s loader — I for one can’t wait.”
— Jeffrey Richter, CLR via C# (Second Edition) p. 164 (Emphasis mine)
As this change still hasn’t been implemented, I think it’s safe to assume that Microsoft had back-tracked on this intent, and it is perhaps too late to change this now. I tried to search around the web to find out what happened with these plans, but I couldn’t find any answers. I still wanted to get to the bottom of it.
So I emailed Jeff Richter and asked him directly — I figured if anyone knew what happened, it would be him.
He replied within 12 hours, on a Saturday morning no less, and clarified that the .NET 1.0 Beta 1 loader did implement this ‘automatic roll-forward’ mechanism of picking up the latest available Build and Revision of an assembly, but this behaviour was reverted before .NET 1.0 shipped. It was later intended to revive this but it didn’t make it in before the CLR 2.0 shipped. Then came Silverlight, which took priority for the CLR team, so this functionality got delayed further. In the meantime, most of the people who were around in the days of CLR 1.0 Beta 1 have since moved on, so it’s unlikely that this will see the light of day, despite all the hard work that had already been put into it.
The current behaviour, it seems, is here to stay.
It is also worth noting from my discussion with Jeff that AssemblyFileVersion was only added after the removal of the ‘automatic roll-forward’ mechanism — because after 1.0 Beta 1, any change to the AssemblyVersion was a breaking change for your customers, there was then nowhere to safely store your build number. AssemblyFileVersion is that safe haven, since it’s never automatically examined by the CLR. Maybe it’s clearer that way, having two separate version numbers, with separate meanings, rather than trying to make that separation between the Major/Minor (breaking) and the Build/Revision (non-breaking) parts of the AssemblyVersion.
The moral is that if you’re shipping assemblies that other developers are going to be referencing, you need to be extremely careful about when you do (and don’t) change the AssemblyVersion of those assemblies. Any changes to the AssemblyVersion will mean that application developers will either have to re-compile against the new version (to update those AssemblyRef entries) or use assembly binding redirects to manually override the binding.
Just take another look at the version attributes on mscorlib:
// Assembly mscorlib, Version 18.104.22.168
Note that it’s the AssemblyFileVersion that contains all the interesting servicing information (it’s the Revision part of this version that tells you what Service Pack you’re on), meanwhile the AssemblyVersion is fixed at a boring old 22.214.171.124. Any change to the AssemblyVersion would force every .NET application referencing mscorlib.dll to re-compile against the new version!
written on 22-Jul-2009
6opuc [http://6opuc.wordpress.com] says:
Thanks for clarification!
written on 08-Feb-2010
I've recently tested versioning and found that the number only matters if the assembly is signed.
If the assembly was not signed then I was able to change any of the AssemblyVersion and AssemblyFileVersion numbers, rebuild, then overwrite the existing DLLs without issue (unless I changed an interface obviously).
You say that the AssemblyVersion should not be changed for backwards compatability - that does not seem to affect the tests that I have done (with unsigned assemblies). What are your thoughts on that?
written on 10-Apr-2010
Interesting point. Thanks for pointing this out. I was not aware that AssemblyVersion was only taken account of for signed assemblies, but this is indeed the case. This seems quite strange and I'm not sure what the reasoning for this is.
I guess my thoughts are that anything you are "releasing" should be signed, so the implications of changing AssemblyVersion remain as described. (And for anything that you are not "releasing" externally, presumably you don't care about breaking changes anyway...)