Redirecting Dependent Assembly Versions In .NET
With the release of Windows Vista there has been a large number of additions and changes made to the existing Windows common controls. Unfortunately many of these changes require that you use version 6.0 of the common controls library. This is not the version WinForms apps will use by default.
This article will discuss one solution for changing the version of a dependent assembly an application will use. For example purposes this article will discuss how to redirect a WinForms application to use a specific version of the unmanaged common controls library. The concept can be applied to any unmanaged library or assembly that an application may depend upon.
How .NET Finds a Dependent Assembly
Loading an assembly is a two step process for the runtime. The first step is to identify the specific version to load. The second step is to find the appropriate assembly. A full discussion is beyond the topic of this article so I recommend you read the book Essential .NET: Volume 1 for a more detailed discussion. The MSDN topic How the Runtime Locates Assemblies also covers this in detail.
When an assembly needs to be loaded the loader first tries to determine which version to load. If this is an assembly contained in the metadata (through a reference in the IDE) then the entire assembly information is available including the version number. If you call Assembly.Load explicitly then the loader only has the information you provide. Assuming a version is specified the loader will now look for the the appropriate assembly. Note that only strongly named assemblies have version matching done. For non-strongly named assemblies the first assembly found will be used.
Once the loader has identified the version to load it then goes through the process of finding the assembly. For strongly named assemblies the loader will look in the GAC first. If the appropriate version is not found then the loader continues using the standard search path. The search path, slightly configurable, includes the application directory and a few child directories based upon the assembly name. If the loader finds the assembly then it will try to load it otherwise it will fail the call.
The above discussion is a simplification of the process. Refer to the resources mentioned earlier for full details.
A Typical Scenario
Let us set up a typical application architecture so we can have a better understanding of the issues involved in assembly versioning. We will have the following solutions set up.
The SharedCompanyBusiness assembly is a binary reference to a company-wide assembly used in all products. It is not under our direct control and is versioned and released independently. It is not strongly named nor is it stored in the GAC. Products must compile against the version they are most compatible with. The shared assembly is stored with the application during installation. It is currently at v3.5.
ThirdPartyControls is a strongly named assembly stored in the GAC. It contains some UI controls the application uses. It is currently at v10.0.
The two addin projects are built as part of the main solution but they are actually maintained separately. Whenever a new version of the application is released then customers get new versions of the addins but the dev teams responsible for the addins can released interim versions as well.
All references other than the third party and shared assemblies are project references. All the projects are currently v1.0.
Conflicting Assembly Versions
Non-strongly Named Assemblies
The above scenario is pretty common and will work as designed. But now we will introduce a change into the application that will have a ripple effect. SharedCompanyBusiness is updated to v4.0 and new classes are added. The two addins are updated to use the newer version because they need some of the functionality it exposes. The addins need to be released but with the newer shared assembly. We have a problem.
The problem is that the application itself uses v3.5 but the addins are expecting v4.0. Since the shared assembly is not strongly named version numbers do not matter. If we ship the updated version of the shared assembly with the addins then the application will be using v4.0 even though it was never tested against that version. Provided v4.0 is backwards compatible with v3.5 the application will run fine. If the addins do not update the shared assembly then they will crash at runtime because they will attempt to use a type or member that does not exist in the assembly. The worse possible situation is when v4.0 makes a breaking change to the code, such as removing a type. We are then in a no win situation as we cannot use either version without causing a crash.
In summary, for non-strongly named assemblies no version checking is done. The first assembly that is found is used irrelevant of whether the code was compiled against it or not. This can cause runtime crashes if the assemblies are not compatible.
Strongly Named Assemblies
Now suppose that ThirdPartyControls is updated from v10.0 to v11.0. This is a strongly named assembly and resides in the GAC. The GAC allows for side-by-side versioning of assemblies. Therefore irrelevant of what versions are installed the application will still want to use v10.0. This is great if the version is installed but suppose it gets uninstalled. In that case the resolver will know to look for v10.0 but it will not find it. The resolver will fail the call. It does not matter whether a newer version is available or not.
In some cases it is reasonable to use a newer version of an assembly if it is available. Security fixes that result in a patched assembly come to mind. The loader does not know the assembly versions are compatible so it will always fail the call. You have to tell the loader that it is OK to use the newer version. To do that you create a versioning policy inside the application’s config file. Here is the entry we would place in CoolApp’s config file to tell it that it should use v11.0 of the ThirdPartyControls library.
The assemblyIdentity element identifies the specific assembly we care about. For strongly named assemblies this will be the assembly name, the public key and any other information we would like to use to distinguish the assembly from other variants. One attribute that was not included here is the type attribute. The type attribute indicates the processor type of the assembly such as x86, msil or amd64. It is useful for differentiating between processor architectures.
The bindingRedirect element tells the loader that instead of using v10.0 of the assembly we should instead use v11.0. This allows us to use newer versions of an assembly even though we compiled with an older version. Of course if the versions are not compatible then a runtime error will occur.
Wildcards are not allowed in the oldVersion attribute but you can specify a range of versions like so: 10.0.0.0-10.99.99.99. This allows you to redirect all versions of an assembly to a newer version.
The biggest issue with versioning policies is that it must be applied to each application. If the shared assembly is used by many applications, which is primarily why you would store it in the GAC, then it can be a hassle to update each application. For some changes, such as security fixes, you can be confident in compatibility and you want to force everyone to use the new, more secure, version. As the publisher of the assembly you can create a publisher policy.
A publisher policy is added to the updated assembly, as a manifest, and stored in the GAC. The publisher policy works just like a versioning policy except it applies to all applications irrelevant of whether they realize it or not. It is ideal for security fixes. In fact the .NET framework uses this approach when releasing service packs. When a service pack is released a publisher policy is included with it. This causes all applications to use the new service packed version irrelevant of what version they were compiled against.
Mixed Assembly Versioning
The above discussion handles the original topic of this article but a few additional topics are worth mentioning. In the original scenario there was not a situation where an assembly was referenced more than once within a single project. We will modify the scenario to introduce this issue.
In this modified scenario the addin logic has been moved from CoolBusiness to CoolAddin. The addin projects now reference the CoolAddin project. CoolAddin requires some core logic so it references CoolBusiness. CoolBusiness is removed from the addin projects as the core logic is in CoolAddin. CoolApp requires access to the addins and the business logic so it references both CoolBusiness and CoolAddin.
Technically CoolApp has two references to CoolBusiness: one explicit and one implicit through CoolAddin. This is where mixed assembly versions can cause problems. If CoolAddin uses a different version of CoolBusiness than CoolApp (possible if the projects were developed by different teams) then the compiler will not know which version to use. The compiler will generate a warning in this case but the warning might be a little confusing. The warning will say that CoolApp is using version X of assembly CoolBusiness but it depends on an assembly that uses version Y. Ideally this should have been an error because it will cause untold suffering at runtime but there are very rare occasions where the message can be safely ignored.
If you ever get this warning then you need to fix it. The problem will manifest itself at runtime in one of several different ways. One of the more common ways is for a TypeLoadException to occur when trying to load a type from the assembly. The exception will say that the type does not exist but using Reflector you will be able to verify that it does. Another common exception is an InvalidCastException when you try to assign a variable of type A to an instance of type A where type A is defined in the conflicting assembly. What is going on?
First a couple of important points about the runtime. Firstly the loader will only load an assembly once. Once it is loaded subsequent requests for the same assembly will result in the original assembly being returned. A consequence of this is that the first assembly that the loader finds a match for will be the one it uses.
The second point is that for strongly named assemblies with fully versioned names the loader can load multiple versions of the same assembly. The runtime uniquely identifies a type by its fully scoped name, including assembly. A type called Utility in namespace MyCompany.MyProduct of assembly A is distinct from the MyCompany.MyProduct.Utility type in assembly B. The runtime knows the differences as they were fully generated during compilation. You cannot automagically redirect a type to a different assembly without the runtime throwing an exception.
Do you see the problem yet? If CoolApp loads v1.0 of CoolBusiness but CoolAddin tries to load v2.0 it will not work. Since the assemblies are not strongly named whichever one gets loaded first wins. In the case of v1.0 being loaded CoolAddin will likely try to load a type that does not exist and, hence, get an exception. Of course if CoolApp was compiled with v2.0 and CoolAddin used v1.0 then things would likely work, for now at least.
If we now turn our attention to strongly named assemblies we can see the other common problem with mixed assembly versions. Suppose for a minute that CoolBusiness was strongly named. During compilation CoolApp used v1.0 but CoolAddin used v2.0. Because it is strongly named two copies (v1.0 and v2.0) of CoolBusiness could be loaded at the same time. But that would mean we have two copies of every type that is shared by the two versions. Which one gets used at runtime depends upon where it is referenced. Anywhere inside CoolApp would use v1.0 while anywhere inside CoolAddin would use v2.0. Provided they remained separate things would work but this is unlikely. Instead it is more likely that eventually CoolAddin would pass a v2.0 object to CoolApp, which expects v1.0, and we would get a type exception. What makes this hard to trace down is that even in the debugger we would see that the object is of the correct named type but the full typename would not match.
To avoid the issues of mixed assembly versions ensure that all your projects use the same dependent assembly versions. If necessary use versioning policies to enforce this.
We have come full circle but we have not addressed the original example of the article: how do we force a WinForms app to use newer versions of Common Controls, a Win32 library? We have already learned how to do it. We just need to apply it. We will create a versioning policy that tells our application to use the desired version, v6.0 in this case, rather than the standard version. The problem is that we are not loading an assembly but a Win32 library. Therefore we will place the versioning information in an application manifest and store it in the assembly directly. The manifest will be located and read by the loader before our application gets far in the loading process. The syntax is identical to the configuration file. Here are the steps for Visual Studio 2008 (VS2005 has a few additional steps).
- Add a manifest file to your executable project if you do not already have one. The application manifest is an item in the Add New Items dialog for the project.
- Modify the assemblyIdentity and description elements to match your application.
- Add the following XML fragment to the manifest.
Compile and run the application and the appropriate version should be loaded. You can do this with any Win32 library that supports side-by-side versioning. The hard part is determining the values to use. The easiest way is to load the code up in the debugger and get the library from the debugger. The type will be win32 for Win32 libraries.
Note that as of VS2008 SP1 Beta it appears that v6.0 of the Common Controls library is automatically used for v2.0 applications. Still this technique can be used for other native libraries as well.