Search code examples
c#json.netnugetassembly-binding-redirect

Why is an assembly binding redirect required for this project?


I've used https://icanhasdot.net to analyze the NuGet dependencies of a sample project I've been experimenting with. Here are the results: enter image description here

The graph doesn't show specific versions, but I'm targeting .NET Framework 4.5 for the project and based on the "Manage Nuget" view in Visual Studio I know that all the NuGet Packages (i.e. all the green squares in the graph) in my project require Newtonsoft.Json >= 6.0.8, e.g.:

enter image description here

I want to use a slightly newer version of Newtonsoft.Json for my project, so I've added version 8.0.2. Since this is definitely >= 6.0.8 I wouldn't expect this to cause problems. However, when I run the program I immediately get a System.IO exception saying that Newtonsoft.Json 6 something was not found.

I've fixed this problem by adding an app.config file with an assembly binding redirect (Assembly Binding redirect: How and Why?) to the newer version of Newtonsoft.Json and this fixed the problem. However, I don't understand why such a binding would be required if all the NuGet packages my project depends on require >= 6.0.8, which 8.0.2 definitely is.


Solution

  • The .NET runtime has a concept called strong naming.

    I'm probably getting lots of technical details wrong, but basically an assembly that is not strongly named effectively says "my name is zivkan.utilities.dll", and when another assembly is compiled against my assembly, the reference says "I need the class named zivkan.utilities.thing from zivkan.utilities.dll". So, it knows nothing about versions and you can drop in any zivkan.utilities.dll that contains a zivkan.utlities.thing class and the runtime will try to run it.

    If I strong name sign zivkan.utilities.dll, now the assembles advertises itself as "my name is zivkan.utilites.dll version 1.0.0 with public key ..." (I'm going to leave the public key part out for the rest of my answer). Now, when another assembly is compiled against it, the compiled reference says "I need zivkan.utilities.dll version 1.0.0". Now when this execute, the .NET runtime will only load zivkan.utilities.dll version 1.0.0 and fail if the version is different, like you saw, you get an error. The program can have a binding redirects to tell the .NET runtime assembly loader that when it sees a request for zivkan.utilties between the versions of 0.0.0.0 and 2.0.0.0 to use version 2.0.0.0, which is how you solved your problem.

    NuGet versions and assembly versions are two separate concepts, but since they're typically the same value (or a very similar value), it's not so different. Assembly versions are a run-time thing, while NuGet package versions are a build-time thing.

    So, imagine the situation without binding redirects. Your program, CommandLineKeyVaultClient is loaded and has a dependency on Newtonsoft.Json version 8.0.2. The .NET runtime loads Newtonsoft.Json.dll and confirms that it is indeed version 8.0.2. Then the .NET runtime sees that CommandLineKeyVaultClient also has a dependency on Microsoft.Rest.ClientRuntime.dll, let's say version 1.0.0.0. So, the .NET runtime loads that dll and confirms the assembly version number. Microsoft.Rest.ClientRuntime.dll has a dependency on Newtonsoft.Json.dll version 6.0.8. The .NET runtime sees that Newtonsoft.Json version 8.0.2 is already loaded, but the version doesn't match and there's no binding redirect, so let's try to load Newtonsoft.Json.dll on disk (there's actually a hook you can use to tell the loader to load the dll from a different directory, when you really need to load different versions of the same assembly, you can). When it tries, it sees the version of the assembly doesn't match the strong named dependency, and fails saying "can't load Newtonsoft.Json.dll version 6.0.8", which is true because the version on disk is actually 8.0.2.

    If you use NuGet packages using PackageReference, NuGet will look not only at transitive NuGet dependencies, but also project dependencies and build a graph of all assemblies (project or nuget) that are needed. Then MSBuild should automatically detect when two different assemblies depend on different versions of the same assembly name and generate binding redirects. Therefore, when using PackageReference, this should not generally be a problem.

    However, if you use packages.config to define your NuGet dependencies, NuGet will try to add binding redirects when it detects a version conflict (which I think you can opt-out of). But since this is calculated at the time you modifiy NuGet dependencies in that project(install, upgrade or uninstall a package), it's possible to get the binding redirects out of sync, and there's an issue with project to project dependencies and what NuGet packages those project references use.

    Anyway, I hope this explains why you get the dll loading error when all your projects have NuGet dependency >= 6.0.8. Again I repeat that assembly versions and NuGet versions are different things, even when they have the same value, and the .NET runtime allows you to load different versions of the same assembly at the same time and needs instructions when you don't want that, which is what binding redirects are.