Important: The information below applies specifically to .Net Framework applications. Most of the information also applies to .Net Core and Standard applications as well. A future post will examine Core/Standard applications.

The Intro

If you develop and/or consume NuGet packages in solutions of reasonable complexity, you'll eventually come up against an assembly that doesn't load, giving this error:

System.IO.FNeLoadException: 'Could not load file or assembly 'NewtonsoftJson, Version=9.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: Ox80131040)'

What causes this error? What can you do? What should you do?

This isn't a definitive reference, but it steps through a simple prototype app that reproduces the problem and shows paths forward.

The Cause

Fundamentally, during runtime your solution is trying to load two different assembly versions at the same time.

A Visual Studio solution can be made of multiple projects, each project can have dependencies on other projects as well as assemblies installed via NuGet. In a setting where teams share code, projects in a solution could "belong" to different teams, making it challenging to keep all package dependencies up-to-date and tested.

Microsoft has a mechanism for helping resolve these dependencies: Assembly Binding Redirects. Essentially, information is added to the project's web.config or the built app.config that says, "If the requested assembly version is within this range, load this specific assembly version."

<dependentAssembly>
	<assemblyIdentity name="PackageFTest" publicKeyToken="80F6849EB416DBC5" culture="neutral"/>
	<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0"/>
</dependentAssembly>

Binding redirects are used when the assembly is strong-named. A strong-named assembly is uniquely identified by its name and assembly version using a key, which the consuming assembly knows about. The purpose of strong-naming is to mitigate malicious replacement of assemblies (i.e. dlls).

In short, strong-naming causes the "could not load assembly" error.

  1. Strong-naming NuGet package assemblies forces the consuming assembly to load a specific version.
  2. The project you're building can only have one copy of the assembly in its bin folder.
  3. If two different versions of the assembly are required, binding redirection may say "it's OK to load this other version."
  4. But if that doesn't work, an error is thrown.

The Solution(s)

Strong-naming NuGet package assemblies is recommended for open source projects for the reason stated above: tamper mitigation. In .Net Core, Microsoft is moving toward strong-naming as a default. However, Core projects handle strongly-named dependencies differently than Framework. By default, they automatically resolve to use the latest assembly version.

But for internal organization projects, strong-naming in Framework or Core projects may not be needed, which can save a lot of grief.

The basic solutions are:

  1. Keep your solution package versions matching at all times. This is the simplest solution, and arguably should be happening anyway as part of solution maintenance.
  2. Remove strong-naming from your NuGet package assemblies. This is the simplest preventative solution. Note that you still need to be concerned with strongly-named packages your package consumes, e.g. NewtonsoftJson.
  3. When strong-naming in Framework or Core, only change the major assembly version when the package major version changes. For example, package versions 2.1.3 and 2.54.7 would both keep the assembly version at 2.0.0. This is how NewtonsoftJson, and Microsoft, manage versioning, and it aligns with NuGet's preference for Semantic Versioning.

    There should be no breaking changes except in major versions.

The Glossary, a Few Rules, and Some Surprises

The Four Versions

NuGet Package Version

  • No effect on runtime behavior
  • Recommended: Use Semantic Versioning (SemVer)

Assembly Version

  • Affects runtime behavior, especially if using strong naming
  • Recommended: Only change the major version to match the package Version. Leave other numbers at 0.

File Version

  • No effect on runtime behavior
  • Used by Windows
  • Must have all four numbers filled in
  • Recommended: Keep major.minor.patch in sync with package version, and revision = 0.

Informational (Product) Version

  • Must be added manually to the AssemblyInfo.cs file (boo, hiss).
  • No effect on runtime behavior
  • String value
  • Recommended: Set to package version

How AssemblyInfo and NuSpec version maps to File Properties

Strong Names

  • Purpose is to guarantee a unique identity for a version and reduce file tampering.
  • Allows side-by-side loading, but practically-speaking on when installed to the GAC.
  • No certificate, so no end date and not an authentication mechanism
  • A strongly-named assembly cannot depend on a weakly-named assembly
    • It is possible to strongly-name a third-party assembly with your own strong-name key (snk)
  • In Framework:
    • Strong naming enables strict assembly loading, meaning .Net expects to only load the exact version.
    • To make this work when dependency resolution chooses a different version, binding redirects are required.
  • In Core, strict assembly loading isn't used. .Net will load the latest usable version.

Some reasons NOT to strong-name an assembly (taken from https://softwareengineering.stackexchange.com/a/240941):

  1. Because strong-named assembly can only load other strong-name assemblies. - a real problem if you rely on 3d party libs.
  2. When the assembly is strong-name it's requires to have the versions, so you can't use direct bindings between your projects in the same solution or you'll have to recompile the application to be able to use a different version.
  3. Apart from the obvious inconvenience, there's also some performance issues (not significant though) because of verification of the signature. (If you interested to know the performance issue in depth , read here strong name by Richard Grimes

P.S. C#: why sign an assembly?

Assembly Binding Redirects

  • Entries in project web.config and built app.config saying which assembly version to load
  • In desktop--i.e. app.config--projects, if Project Properties > Application > Auto-generate binding redirects is check, redirects are automatically added to the assembly's output app.config when the project is built.
  • Web--i.e. web.config--projects do not have an Auto-generate binding redirects setting because the dependencies are always in the project's bin folder.
  • Binding redirects can be updated using the NuGet PowerShell command Add-BindingRedirect.

Web Applications Are More Susceptible

I don't know why, but I wasn't able to reproduce the binding problem using a console application. Maybe I never did it right (or wrong). However, web applications are more likely to have the problem because redirects are not automatically generated.

This was a surprise to me, and probably catches many developers.

When Are Potentially Dependent Assemblies Added to the Bin Folder?

  • Package reference assemblies are always added to the bin folder. They're explicit dependencies.
  • Project reference assemblies are added to the bin folder if they're called. As you'll see below, if ProjectA depends on ProjectB with consumes PackageFTest, but there's no code path from ProjectA that requires PackageFTest, PackageFTest.dll won't show up in the bin folder.

The Proof

The following Framework solution is very simple. Its only purpose is to reproduce the version assembly loading problem.

  • There's one NuGet package with one method.
  • The package code never changes. The only change is the versioning and strong-naming.
  • The web application depends on two projects.
  • Each of those two projects has a different package version installed.

Create a new solution with the following. In my case, all were .Net 4.7.2 projects.

  • ASP.NET MVC application named WebApplication1
  • Two class libraries named ClassLibrary1 and ClassLibrary2
  • A class library named PackageFTest.

    The unusual name is to prevent accidentally installing a package from NuGet.org named...Package1.

PackageFTest > Class1.cs

    public class Class1
    {
        public int GetId() => 1;
    }

ClassLibrary1 > Class1.cs

     public class Class1
     {
      // public int GetId() => new PackageFTest.Class1().GetId();
     }

ClassLibrary2 > Class1.cs

     public class Class1
     {
      // public int GetId() => new PackageFTest.Class1().GetId();
     }

WebApplication1 > Controllers > HomeController.cs

    public ActionResult Index()
    {
        // new ClassLibrary1.Class1().GetId();
        // new ClassLibrary2.Class1().GetId();
        return View();
    }

Add ClassLibrary1 and ClassLibrary2 to WebApplication1 as Project References.

Build the solution.

We're going to build and package PackageFTest six times, so in the PackageFTest folder add a command file named bp.cmd with this code:

msbuild PackageFTest.csproj
nuget pack PackageFTest.csproj

We need nuget.exe, which is not included with Visual Studio starting with version 2019. Instead, download the latest stable version and, for this exercise, place it in the Packages folder. Normally, you'd put it in an external folder and add it to the Path.

By design, msbuild.exe isn't added to the Path environment variable. Instead, search for and open the Developer Command Prompt for VS 2019, then change directories to your PackageFTest folder.

Generate a .nuspec file by running nuget spec. Open the PackageFTest.nuspec file and make it look like this. Note that we're allowing token substitution as much as practical. The important thing is that we're explicitly setting the package version. Leave the nuspec file open for editing.

<?xml version="1.0" encoding="utf-8"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>1.0.0</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <description>$description$</description>
    <releaseNotes></releaseNotes>
    <copyright>Copyright 2020</copyright>
  </metadata>
</package>

We'll be setting the package's assembly information several times. Instead of doing this the normal way through Project Properties, open PackageFTest\Properties\AssemblyInfo.cs and set these fields.

The AssembyInformationalVersion attribute must be added manually. This is very useful because you can store the NuGet package version, which could potentially be something like 2.0.0-beta1, and be able to view it in file properties.

[assembly: AssemblyTitle("PackageFTest")]
[assembly: AssemblyDescription("PackageFTest Description")]
[assembly: AssemblyCompany("c flatt")]
[assembly: AssemblyProduct("Package F Test")]
[assembly: AssemblyInformationalVersion("3.1.0")]
[assembly: AssemblyCopyright("Copyright ©  2020")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Let's simplify working with our package. Right-click the solution and choose "Manage NuGet Packages for Solution"

Open the package source settings.s

Add a package source that points to your PackageFTest folder, then uncheck the other packages. Now we'll only see our package when it's built and packed.

Let's build all of our package versions. If you followed the steps above, version 1 is ready to build and pack. In the Developer command line, run bp.cmd. The package will be built and packed as PackageFTest.1.0.0.nupkg

Weak-Named Coupled Assembly Versions

Now you're going to edit the four version numbers. As a reminder, the only ones that matter to .Net are the package and assembly versions. Setting the product information helps us later to see which .dll is in the bin folder. You can not set the file version, if you want. If you do, it should match the assembly version.

  • In PackageFTest.nuspec and change <version> to 1.1.0 and Save.
  • In AssemblyInfo, change AssemblyInformationalVersion to 1.1.0. Change AssemblyVersion and AssemblyFileVersion to 1.1.0.0. Save the file.
  • Run bp.cmd to generate PackageFTest.1.1.0.nupkg

Strong-Named Coupled Assembly Versions Open PackageFTest > Properties > Signing. Check "Sign the assembly". Under Choose a strong name key file, choose New, enter a key file name of p1.snk, uncheck Protect my key file with a password, click OK.

  • In PackageFTest.nuspec and change <version> to 2.0.0 and Save.

  • In AssemblyInfo, change AssemblyInformationalVersion to 2.0.0. Change AssemblyVersion and AssemblyFileVersion to 2.0.0.0. Save the file.

  • Run bp.cmd to generate PackageFTest.2.0.0.nupkg

  • In PackageFTest.nuspec and change <version> to 2.1.0 and Save.

  • In AssemblyInfo, change AssemblyInformationalVersion to 2.1.0. Change AssemblyVersion and AssemblyFileVersion to 2.1.0.0. Save the file.

  • Run bp.cmd to generate PackageFTest.2.1.0.nupkg

Strong-Named Decoupled Assembly Versions

I'm using 3.1.0 and 3.2.0 here to ensure my package versions do not match the assembly version.

  • In PackageFTest.nuspec and change <version> to 3.1.0 and Save.

  • In AssemblyInfo, change AssemblyInformationalVersion to 3.1.0. Change AssemblyVersion and AssemblyFileVersion to 3.0.0.0. Save the file.

  • Run bp.cmd to generate PackageFTest.3.1.0.nupkg

  • In PackageFTest.nuspec and change <version> to 3.2.0 and Save.

  • In AssemblyInfo, change AssemblyInformationalVersion to 3.2.0. Keep AssemblyVersion and AssemblyFileVersion at 3.0.0.0. Save the file.

  • Run bp.cmd to generate PackageFTest.3.2.0.nupkg

The PackageFTest folder now has six packages.

In the Visual Studio > NuGet - Solution tab, refresh the page.

With Browse selected, you should see PackageFTest. Now we'll progressively install the packages, run the application, and see the result.

Weak-Named Coupled Assembly Versions

  • Install version 1.0.0 to ClassLibrary1
  • Install version 1.1.0 to ClassLibrary2
  • Uncomment the Class1 or Index code from ClassLibrary1, ClassLibrary2, and WebApplication1
  • Build the solution
  • Run the solution using F5. The web application runs successfully.

In WebApplication1\bin, you can check PackageFTest.dll's properties to see which version was used. Here, I'm using FreeCommander to display the columns. Remember that product version number = package version number, and file version number = assembly version number.

In this case, the latest version was used, and there were no errors because we are not strong-naming the assembly.

Strong-Named Coupled Assembly Versions

  • Install version 2.0.0 to ClassLibrary1
  • Install version 2.1.0 to ClassLibrary2
  • Build and run the solution. The dependency error is triggered at runtime.

Keep the debugger open and check what's in the bin folder.

Again, the latest version is selected. However, due to strong naming, the application is being told it must load assembly version 2.0.0 and 2.1.0, but only one dll can be in the folder.

Back in Visual Studio debugging, view the Error List and enable seeing Warnings.

If you double-click the warning, Visual Studio will add the following assembly redirect to the web.config file. Otherwise you would have to add it manually.

<dependentAssembly>
	<assemblyIdentity name="PackageFTest" publicKeyToken="A9CA41B60F6390A1" culture="neutral"/>
	<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0"/>
</dependentAssembly>

Stop the application, then F5 again and it will run successfully.

Strong-Named Decoupled Assembly Versions

Important Comment out the dependentAssembly xml created in the previous run. We want to prove there's no binding redirection going on.

  • Install version 3.1.0 to ClassLibrary1
  • Install version 3.2.0 to ClassLibrary2
  • Build and run the solution. The project runs successfully.

Why? Because the assembly version of the two dlls is the same: 3.0.0.0.

Note that this time the lower package version number was selected. This is how .Net resolves the dependency when using packages.config. When using PackageReference, the highest usable package version is selected.

This is a problem. What if ClassLibrary2 is using a method named GetId2() that's in PackageFTest that's available in package version 3.2 but not 3.1?

    public int GetId()
    {
        var p = new PackageFTest.Class1();
        return p.GetId() + p.GetId2();
    }

An error is thrown, that's what.

What's our solution? As noted above, the best is to set all projects to use PackageFTest package 3.2. What if you can't? How about a binding redirect in the WebApplication to 3.2.0?

<dependentAssembly>
	<assemblyIdentity name="PackageFTest" publicKeyToken="A9CA41B60F6390A1" culture="neutral" />
	<bindingRedirect oldVersion="0.0.0.0-3.2.0.0" newVersion="3.2.0.0" />
</dependentAssembly>

No, that makes things worse. It prevents the dll from being copied to the bin folder at all.

The admittedly frustrating answer is to install PackageFTest 3.2.0 to WebApplication1. This forces the latest package dll to be copied to the bin folder.

It's a problem of transitive assemblies. A depends on B which depends on C, but A doesn't directly depend on C.

At this point the application runs successfully.

Wrap Up

Automatically resolving dependencies is, apparently, hard. If we were installing multiple assembly versions to the GAC, there wouldn't be a problem. But that's not how modern applications are all deployed. In a sense, we're deploying applications the same way as in the DOS 6 days: the entire application and its dependencies are in a folder. No shared dlls. Except, of course, as seen above, we only get to install one dll copy regardless of its version.

Is this still DLL Hell? Package Hell? No matter how you label it, when things go wrong there's hell to pay.

References