2

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.

Community
  • 1
  • 1
Adam
  • 8,752
  • 12
  • 54
  • 96
  • Newtonsoft.JSON almost certainly follows Semantic Versioning, which means that a major version change does not guarantee backwards compatibility. Your safe choices are any version of Newtonsoft.Json that is still major version 6. For example 6.0.9 or above, but not 7.x.x or 8.x.x. If 8.x.x still works, good for you, but you probably ought to test the sh*t out of it. – Robert Harvey Apr 10 '19 at 18:04
  • You're only looking at the Nuget dependencies, not the assembly dependencies. Each assembly is bound to a specific version of an assembly, particularly those that are strongly named. Redirects allow the runtime to bind to different versions than those that the applicaitons are linked to. – Erik Funkenbusch Apr 10 '19 at 18:06
  • @ErikFunkenbusch what's the difference between a NuGet dependency and an assembly dependency? How can I view the assembly dependencies? – Adam Apr 10 '19 at 18:18
  • @RobertHarvey to your point, if it says that the NuGet requires >= 6.0.2 perhaps that's an indication that it's fine to use a binding redirect here, i.e. even though the assembly has a baked in dependency on 6.x we're being told that it's fine to use anything greater than 6.0.2. – Adam Apr 10 '19 at 18:26
  • That's fair enough. But generally, a major version change is used when you want to signal that you're breaking backward compatibility in some way. – Robert Harvey Apr 10 '19 at 18:29
  • Every assembly has a specific 4 part version. x.x.x.x, while most nuget packages are 3 digit, and have no bearing on how the os binds the assemblies. Assembly binding is low level, and unless there is a binding redirect, if assembly A is linked to version 1.0.0.0 then it will not load an assembly that is a different version. The binding redirect tells the runtime it’s ok to use a different version (or versions) – Erik Funkenbusch Apr 11 '19 at 01:30
  • I suggest reading https://learn.microsoft.com/en-us/dotnet/framework/app-domains/assembly-versioning and https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/redirect-assembly-versions – Erik Funkenbusch Apr 11 '19 at 01:37
  • @ErikFunkenbusch if assembly A is linked to version 1.0.0.0 will it load version 1.0.0.1? Does it have to be a 100% exact match? – Adam Apr 11 '19 at 22:16

2 Answers2

7

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.

buckley
  • 13,690
  • 3
  • 53
  • 61
zivkan
  • 12,793
  • 2
  • 34
  • 51
  • Can you provide a link to docs showing how to set the "hook you can use to tell the loader to load the dll from a different directory, so when you really need to load different versions of the same assembly, you can"? – Adam Apr 10 '19 at 19:37
  • I also wonder if it's possible to look at a NuGet package in my `packages folder` and determine the assembly dependencies? – Adam Apr 10 '19 at 19:38
  • Could you also comment on when it's safe to add a binding redirect? E.g. if the NuGet dependency is for >= 6.0.8, is it safe to redirect to a higher version of the dependency even if the assembly dependency is for 6.0.8? – Adam Apr 10 '19 at 19:39
  • I think in the .NET Framework you want to use [AppDomain.AssemblyResolve](https://learn.microsoft.com/en-us/dotnet/api/system.appdomain.assemblyresolve?view=netframework-4.7.2). In .NET Core you'll want to look at [AssemblyLoadContext](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext?view=netcore-2.2). I think that Mono doesn't support loading different versions of an assembly at the same time. – zivkan Apr 10 '19 at 20:36
  • You can find assembly references using ILSpy, ILDASM or similar tools. To do it programaitically, look for the library that ILSpy uses to disassemble managed dlls. – zivkan Apr 10 '19 at 20:43
  • There's no rule about when it's safe to use binding redirects. If the higher version is backwards compatible with the lower version, then it's safe. If the versions are not 100% compatible, it might still be safe if the methods with the breaking change are never called. When you know different dependencies use an API from different versions of a package with breaking changes, only in this very specific case you know for certain that binding redirects won't work. So, it takes a lot of detailed analysis to be sure. – zivkan Apr 10 '19 at 20:45
  • 1
    Your answer has some mistatkes. Details are in my answer posted on this date, but in sum: a) the .Net Framework enforces a version match, whether or not strong naming is used. b) .Net Core, and v5 and v6, do not enforce a version match. c) strong naming is not used in .Net Core/5/6: it is allowed, but has no meaning for applications in those environments. – Elroy Flynn May 21 '22 at 03:52
  • One of the best answers I've seen here. Let me emphasize the most important part: "when using PackageReference NuGet/msbuild will build a graph (in a file `{ProjectDir}\obj\project.assets.json`) and will automatically generate the binding redirects". – drizin Dec 03 '22 at 16:51
2

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.

Elroy Flynn
  • 3,032
  • 1
  • 24
  • 33
  • 1
    Finally a proper explanation of why these annoying binding redirects are needed. I was searching to find an explanation to no avail until I finally came across to an answer that is neither ticked or marked as useful, your answer (oh the irony). Thanks!! – DimitrisK Nov 23 '22 at 08:52