loading...

Escape from Nuget Hell

timothymcgrath profile image Timothy McGrath ・3 min read

If you're a .NET developer, as Nuget has become more prolific, you have come to dread the following error:

System.IO.FileLoadException: 'Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)'

It sometimes happens while debugging, it sometimes happens after deploying an app. It's not always Newtonsoft, but Newtonsoft is used everywhere, so it's an easy example.

Using a small example project, I'm going to walk through an example that creates the problem and then a couple of solutions to solve the problem.

Example Project

The example is a solution containing two .NET Framework projects (4.7.2). ReferenceTest.Console is a Console app that references ReferenceTest.Logic, a Class Library. They are both going to reference Newtonsoft.Json through Nuget, but they will use different versions of Newtonsoft.Json to replicate different issues.

What Assembly Bindings Fix

In this example, the two projects reference:

ReferenceTest.Console - Newtonsoft 12.0.3

ReferenceTest.Logic - Newtonsoft 9.0.1

This is a pretty common issue where both projects are referencing different versions of a nuget package, but the parent project (Console) also references the higher version of the nuget package. This will error at runtime when code in the child project is called for the first time.

This can be fixed by telling the child project (Logic) to reference the higher-version nuget package as well. (This is technically perilious as it is not true that v12 of Newtonsoft is guaranteed to work the same as v9, but that's an issue for another day).

How do we tell the child project (Logic) to use v12? AssemblyBindings! The AssemblyBinding will tell your app to use v12 of Newtonsoft instead of v9. It can be defined in the configuration file like this:

<runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30AD4FE6B2A6AEED" culture="neutral"/>
        <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0"/>
    </dependentAssembly>
    </assemblyBinding>
</runtime>

The trick to making this work is to activate Auto-generate Binding Redirects in your Project's Properties page (right below the .NET Framework version). Activating this will automatically add the correct binding redirects to your project's app.config (in the debug/release folder, not in your source code).

What AssemblyBindings Won't Fix

ReferenceTest.Console - 9.0.1

ReferenceTest.Logic - 12.0.3

This issue cannot be resolved. This is a case where the child project is referencing a higher version than the parent, so this will error at runtime when the child project is loaded. You can't fix this with an assembly binding unless you configure it to point v12 to v9, which seems especially dangerous to do.

The only way I know to fix this is to update the parent project (Console) to use 12.0.3 as well. This solution works, but it creates a problem where anytime you add or update a nuget package from a lower-level project, you run the risk of creating this error in a parent project, and you won't know about it till runtime.

How Can I Catch This Sooner?

This is where the new Project file format will save us.

The new Project format has numerous advantages. It automatically includes all the files in the folder instead of only ones listed in the project file, it eliminates a number of unnecessary project file attributes, and it improves Nuget references.

Converting the projects by hand is cumbersome, so I use this migration tool.

Running this tool on both projects makes nice, tidy .csproj files using PackageReference instead of a separate packages.config file.

But the best part is a new compile error shows up for our previous example:

Error   NU1605  Detected package downgrade: Newtonsoft.Json from 12.0.3 to 9.0.1. Reference the package directly from the project to select a different version. 
ReferenceTest.Console -> ReferenceTest.Logic -> Newtonsoft.Json (>= 12.0.3) 
ReferenceTest.Console -> Newtonsoft.Json (>= 9.0.1) ReferenceTest.Console

The compiler has figured out that there is an incompatible nuget reference and that we need to upgrade the host project to resolve it. So, now we have our error at compile-time instead of at run-time. Beautiful!

Wrapup

So, we learned that there are two different ways we can get into trouble with nuget package referencing. They can be handled by assemblybindings but for the best experience you need to migrate to the new .csproj format.

Posted on by:

timothymcgrath profile

Timothy McGrath

@timothymcgrath

i'm a .net dev that loves learning new things

Discussion

markdown guide
 

Hey Timothy, great post. I’m using .NET Core 3. Do you know if I could run into the same assembly binding issue too?

So far it hasn’t been a problem for me. But as my project grows, it might be more likely.

 

Thanks Katie!
Good question... As I understand it, you no longer need to do assembly binding in .NET Core. They figured it out to save our brains.