Adam
Adam

Reputation: 10056

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.

Upvotes: 2

Views: 4654

Answers (2)

Elroy Flynn
Elroy Flynn

Reputation: 3218

I think that there are some errors in the selected answer, and in the comment threads. I'll try to clear it up.

First, you must know that package version and assembly version are two completely different things. You could publish version 1.0.0 of a package, and the assemblies in the package could be version 99.0.0.0. That might be a bad practice, but nothing prevents it, and in fact some Microsoft-published packages do this, I'm sure for good reason.

The .Net Framework (meaning NOT (.Net Core, or, .Net v5 or v6)), enforces an exact version match when resolving a reference to a strongly named assembly at runtime, by default. That is, if you build an assembly A that had a build-time reference to v 1.0.0.0 of assembly B, then by default it will require version 1.0.0.0 of assembly B at runtime, assuming that B is strongly named. The only way to defeat that is to use BindingRedirect, or publisher policy, which itself requires use of the GAC.

It's understandable that you thought that the Nuget versioning rule (e.g., >= 1.0.0) would have some relation to the assembly binding mechanism, but it doesn't. It just specifies what newer package versions you will automatically accept when running "NuGet Update". It has no bearing on the runtime behavior of accepting newer versions of assemblies than the version that was used at build time.

.Net v5 and v6 (maybe also Core 1 and 2) applies no meaning to strong naming, and so does not enforce any version match. If you require different behavior, you can use the https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext API.

Strong naming in .Net Framework is principally a tool that lets different versions of an assembly load simultaneously side-by-side in a application domain, at runtime. That capability requires use of the GAC. SN is essentially meaningless in .Net core.

Upvotes: 5

zivkan
zivkan

Reputation: 15082

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.

Upvotes: 13

Related Questions