We talk quite easily about new technologies and things we just learned about, because that's the way geeks work. But for newcomers, this is not always easy. This is a large recurring debate, but I find that it is good to step back from time to time and talk about good practices for these newcomers.
The AssemblyVersion and AssemblyFileVersion Attributes
When we want to give a version to a .NET assembly, we can choose one of two common ways :
- AssemblyVersion, which is stored in the assembly manifest,
- AssemblyFileVersion, which is stored the PE image resources.
Most of the time, and by default in Visual Studio 2008 templates, we can find the AssemblyInfo.cs file in the Properties section of a project. This file will generally only contain an AssemblyVersion attribute, which will force the value of the AssemblyFileVersion value to the AssemblyVersion's value. The AssemblyFileVersion attribute is now added by default in the Visual Studio 2010 C# project templates, and this is a good thing.
It is possible to see the value of the AssemblyFileVersion attribute in file properties window the Windows file explorer, or by adding the "File Version" column, still in the Windows Explorer.
We can also find a automatic numbering provided by the C# compiler, by the use of :
Each new compilation will create a new version.
This feature is enough at first, but when you start having somehow complex projects, you may need to introduce continuous integration that will provide nightly builds. You will want to give a version to the assemblies in such a way it is easy to find which revision has been used in the source control system to compile those assemblies.
You can then modify the Team Build scripts to use tasks such as the AssemblyInfo task of MSBuild Tasks, and generate a new AssemblyInfo.cs file that will contain the proper version.
Publishing a new version of an Assembly
To come back to the subject of versionning an assembly properly, we generally want to know quickly, when a project build has been published, which version has been installed on the client's systems. Most of the time, we want to know which version is used because there is an issue, and that we will need to provide an updated assembly that will contain a fix. Particularly when the software cannot be reinstalled completely on those systems.
A somehow real world example
Lets consider that we have a solution with two assemblies signed with a strong name Assembly1 and Assembly2, with Assembly1 that uses types available in Assembly2, and that finally are versioned with an AssemblyVersion set to 126.96.36.1998. These assemblies are part of an official build published on the client's systems.
If we want to provide a fix in Assembly2, we will create a branch in the source control from the revision 188.8.131.528, and make the fix in that branch which will give revision 460, so the version 184.108.40.2060.
If we let the Build System compile that revision, we will get assemblies that will be marked as 220.127.116.110. If we only take Assembly2, and we place it on the client's systems, the CLR will refuse to load this new version if the assembly, because Assembly1 requires to have Assembly to of the version 18.104.22.1688. We can use the bindingRedirect parameter in the configuration file to get around that, but this is not always convenient, particularly when we update a lot of assemblies.
We can also compile this new version with the AssemblyVersion of 22.214.171.1240 set to 126.96.36.1998 for Assembly2, but this willl have the disadvantage of lying about the actual version of the file, and that will make diagnostics more complex in case of an other issue that could happen later.
To avoid having those issues with the resolution of assembly dependencies, it is possible to keep the AssemblyVersion constant, but use the AssemblyFileVersion to provide the actual version of the assembly.
The version specified in the AssemblyFileVersion is not used by the .NET Runtime, but is displayed in the file properties in the Windows Explorer.
We will then have the AssemlyVersion set to the original published version of the application, and set the AssemblyFileVersion to the same version, and later change only the AssemblyFileVersion when we published fixes of these assemblies.
Microsoft uses this technique to version the .NET runtime and BCL assemblies, and we take a look at System.dll for .NET 2.0, we can see that the AssemblyVersion is set to 188.8.131.52, and that the AssemblyFileVersion is set, for instance, to 2.0.50727.4927.
Other examples of versionning issues
We can find other cases of loading issues linked to the mismatch of the version for a loaded assembly that is different from the expected assembly version.
Custom Behaviors in WCF
WCF gives the developer a way to provide custom behaviors to alter the default behaviors for out-of-the-box bindings, and it is necessary to provide the fully qualified name, without errors. This is a pretty annoying but in WCF 4.x because it is somewhat complex to debug, and it is a very good case of use for the deactivation of "Just My Code" to find out why the assembly is not being loaded.
A good new though, this pretty old bug has been fixed in WCF 4.0 !
Dynamic Proxy Generators
Some dynamic proxy generators like Castle Dynamic Proxy 2 or Spring.NET use fully qualified types to generate the code for the proxy's, and loading issues can occur if the assembly referenced by the proxy is not exactly what is being loaded, with or without a Strong Name. These frameworks are heavily used with AOP, or by frameworks nHibernate, ActiveRecords or iBatis.
To be a bit more precise, the use of the ProxyGenerator.CreateInterfaceProxyWithTarget method generates a proxy that targets the assembly that is referenced during the generation of the code for the proxied interface.
To give an example, let's take an interface I1 in an assembly A1(184.108.40.206), which has a method that uses a type T1 in an assembly A2(220.127.116.11). If we change the assembly A2 and that its version becomes A2(18.104.22.168), the proxy will not be properly generated because the reference T1/A2(22.214.171.124) will be used because compiled in A1(126.96.36.199), regardless if we loaded A2(188.8.131.52)
The best practice of not changing the AssemblyVersion avoid loading issues of this kind. These issues are not blocking, but this more work to do to get around it.
And You ?
This is only a example of "Best Practice", which seems to have worked properly so far.
And you ? What do you do ? Which practices do you use to version your assemblies ?