Some problems in programming seem to stay and bother us forever. Much like cockroaches, these problems resist technological advancements and increasing human knowledge. One such problem is the infamous DLL Hell (and variations of it).

The original DLL hell issue was this: Several applications use a shared DLL file. Then, one of the applications updated the DLL file and now the other applications no longer work. In .NET, this issue is solved. We will usually ship DLL files separately for each application. Alternatively, we can use the GAC for shared DLL’s, which supports versioning. This means we can store different versions of the same DLL and the different applications will load their intended version.

In the modern world, we are dependent on dozens of libraries. These in turn, depend on dozens more and we end up with hundreds or thousands of dependencies. So now the problem arises for a single application. What to do when several of its projects depend on different version of the same assembly?

Here’s an example: project A might use log4net V1.1 and project B uses log4net V1.2. Both DLL files are copied to the output folder, but there can be only one log4net.dll file. As a result, our application will fail at runtime when trying to load the version that wasn’t copied.

Here’s another scenario: We reference project A with NuGet which references System.Net.Http v4.5. We also reference project B with NuGet which references System.Net.Http v4.0. This phenomenon is knows as NuGet Hell. The result is the same and our application will fail with:

If you ran into this exception, you are in the right place.

Luckily, Microsoft put a lot of thought into this issue and there are a lot of things we can do about it.

If possible, resolve to a single reference version

The best solution is to remove the need for different reference versions. Sometimes it’s as easy as going to NuGet manager and changing the versions. This is widely referred as DLL Hell and isn’t in the scope of this article. Some resources to help with DLL Hell are: [1], [2], [3]

Sometimes tough, you can’t depend on a single reference version due to all kind of reasons. The references might be referenced by a 3rd party that you can’t edit or you might have limitations like .NET framework target. If you’re in one of those times, this article is for you.

Use a single reference versions or load versions side-by-side

First of all, there’s an important decision to make: Do we want to force our projects to use the same reference version? Or do we want to load different versions side by side and use both?

The CLR does supports side-by-side loading. which means multiple versions of the same assembly are loaded and act as different modules. This can be problematic. Each assembly version doesn’t expect there’s another instance of it loaded. The different versions might fight for resources or get in each other’s way somehow.

Having said that, you can’t always use a single version, since the referencing projects might rely on features that exist only in their respective referenced versions.

In solution #1, we will how to force a specific version with Binding Redirects. Solution #2 through #4 will show various methods to achieve side-by-side loading.

If possible, prefer Binding Redirect to side-by-side loading

Solution 1: Use a single assembly version with Binding Redirect

In our log4net example, project A uses log4net 1.2.11 and project B uses log4net 1.2.15. We’ve noticed from the exception screenshot above that log4net v1.2.15 fails to load. When the debugger breaks on the Exception, we can open the Modules window (Debug -> Windows -> Modules) and see which version is actually loaded (If any)

The Modules shows that log4net 1.02.11 is loaded.

We can then force our project to use the loaded assembly with binding redirect. To make this work, we’ll need to add the following code to App.config of the assembly referencing log4net 1.02.15. If App.config doesn’t exist, create it.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 <? xml version = "1.0" encoding = "utf-8" ?> < configuration > < startup > < supportedRuntime version = "v4.0" sku = ".NETFramework,Version=v4.6.1" / > < / startup > < runtime > < assemblyBinding xmlns = "urn:schemas-microsoft-com:asm.v1" > < dependentAssembly > < assemblyIdentity name = "log4net" publicKeyToken = "669e0ddf0bb1aa2a" culture = "neutral" / > < bindingRedirect oldVersion = "0.0.0.0-5.0.0.0" newVersion = "1.2.11" / > < / dependentAssembly > < / assemblyBinding > < / runtime > < / configuration >

Note that the runtime section is the one to be added.

We basically tell the runtime that whenever log4net in versions 0.0 – 5.0 is required, use version 1.2.11 instead.

Strong names and the GAC

As mentioned, the CLR program has the ability to load different versions of the same assembly. It’s easier to do that by signing the assembly with a strong name. This creates a unique identity to the assembly and allows to use some elegant solutions to work with different versions.

When using strong-named assemblies, they can be registered to the Global Assembly Cache (GAC). This is a sharing mechanism for common assemblies. It’s also very convenient for storing and loading different versions.

Strong name signing has some disadvantages as well.

We’ll see how to use the GAC to load different assemblies side-by-side later on in Solution 4. If you can’t or don’t want to sign the references with a strong name you can use AssemblyResolve.

Solution 2: Override AssemblyResolve for side-by-side loading (No need for strong names)

Let’s say we have 2 assemblies called Lib1 with different versions 1.0 and 1.1, which are referenced by different projects.

Let’s set CopyLocal to False for both references. We’ll need to copy the assemblies to different subfolders in the output folder, say to V10\Lib1.dll and V11\Lib1.dll. It’s important to have the DLL files in the output folder since this is the folder that will be deployed.

Copying the .dll files can be done with a post-build event (see explanation and some examples). The output folder should look like this:

Since the assemblies aren’t in the output folder (they are in sub-folders), the assemblies will fail to load. Let’s register to AssemblyResolve event, which fires when an assembly’s failed to load, like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void Main ( string [ ] args ) { AppDomain . CurrentDomain . AssemblyResolve += ( sender , resolveArgs ) = > { string assemblyInfo = resolveArgs . Name ; // e.g "Lib1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" var parts = assemblyInfo . Split ( ',' ) ; string name = parts [ 0 ] ; var version = Version . Parse ( parts [ 1 ] . Split ( '=' ) [ 1 ] ) ; string fullName ; if ( name == "Lib1" && version . Major == 1 && version . Minor == 0 ) { fullName = new FileInfo ( @ "V10\Lib1.dll" ) . FullName ; } else if ( name == "Lib1" && version . Major == 1 && version . Minor == 1 ) { fullName = new FileInfo ( @ "V11\Lib1.dll" ) . FullName ; } else { return null ; } return Assembly . LoadFile ( fullName ) ; } ;

On each failed assembly load, we will parse the required version and manually load the assembly from the subfolders.

This is the only solution I was able to implement to load multiple version of an unsigned assembly. But, if we are working with strongly named assemblies, there are more elegant solutions.

Solution 3: Copy assemblies to different folders and use <codebase> to load them side-by-side (Requires strong names)

Let’s take a similar scenario. Suppose we have 2 assemblies called StrongNameLib with versions 1.0 and 1.1, which are referenced by different projects.

We’ll need to set CopyLocal to False for both of them and copy them to subfolders in the output folder with a post-build event. So far, exactly like in the previous solution.

We’ll need to find out the public key token, which is easy enough. Now, we can edit App.config (or create it, if it doesn’t exist) and add <codebase> nodes in the following way:

1 2 3 4 5 6 7 8 9 10 11 <? xml version = "1.0" encoding = "utf-8" ?> < configuration > < assemblyBinding xmlns = "urn:schemas-microsoft-com:asm.v1" > < dependentAssembly > < assemblyIdentity name = "StrongNameLib" culture = "neutral" publicKeyToken = "6a770f8bdf3d476a" / > < codeBase version = "1.0.0.0" href = "StrongNameLibV10/StrongNameLib.dll" / > < codeBase version = "1.1.0.0" href = "StrongNameLibV11/StrongNameLib.dll" / > < / dependentAssembly > < / assemblyBinding > < / runtime > < / configuration >

We’re basically telling the CLR where to go to resolve each version.

The path can also be an absolute path in the format “FILE://c:/Lib/MyLib.dll”.

Resolving with <codebase> like this only works with strongly typed assemblies. When the assembly isn’t strongly typed, the version property is ignored and the first <codebase> node is taken.

Solution 4: Install assemblies to the Global Assembly Cache (GAC)

This is the recommended solution for common libraries since the GAC will share libraries across applications. You can a install both assemblies to the GAC with gacutil.exe. It’s a matter of a single command prompt execution. For example:

1 gacutil . exe - i "c:\Dev\Project\Debug\MyLib.dll"

Read more on using gacutil in this tutorial. Note that there’s a different gacutil for each .NET version.

Once installed, set the references` CopyLocal property to False, since there’s no need for them to be in the output folder. This is it, we’re done. According to the runtime lookup order, the CLR will look in the GAC before checking the output folder. Since both assemblies are installed in the GAC, the runtime will find and load the correct versions.

The only caveat here is that unlike in the other solutions, where the output folder could be deployed as is, you’ll have to take care to install the assemblies in the GAC of the deployed computer. The installation could be included in the installer, installed manually on the cloud or added to the continuous deployment process.

Solution 5: Repack your libraries into a new assembly

Let’s assume you have the following conflict situation:

If you want to load both version of Newtonsoft.Json side by side, you can use ILMerge or il-repack to pack one of the branches into a new assembly (I recommend il-repack out of the two). Those solutions will take several input assembly and merge them into a new assembly. In this case, you can take Library A + Newtonsoft.Json 9 and merge into a new assembly called XYZ.dll. Once done, you can reference XYZ.dll and Newtonsoft.Json 8 without any conflict. Alternatively, you can pack Newtonsoft.Json 8 into a new assembly QWE.dll and then reference Newtonsoft.Json 9 and QWE.dll without any conflicts. This solution doesn’t require strongly-named assemblies.

Summary and resources

We saw the new version of DLL Hell and a lot of ways to deal with it. As mentioned, these solutions should be used with caution.

Here are more resources on the subject:

Share:

Enjoy the blog? I would love you to subscribe! Performance Optimizations in C#: 10 Best Practices (exclusive article) SUBSCRIBE