Nuget Packaging Series 2: Beware of DLL version conflicts

Now that we know how to put multiple assemblies into the same nuget package, we should also be aware of possible downsides.

Putting assemblies with different TFMs into the same package can lead to subtle assembly loading errors at runtime.

The Problem Scenario

Imagine two projects, Project1 targets net48, Project2 targets netstandard2.0.
Both projects have a package reference to Microsoft.Extensions.Logging.Abstractions 6.0.1.
You create a nuget package that has both Project1 assembly and Project2 assembly in it.
You then consume the package in an application with TFM net48.
The consumer application does not directly or transitively reference Microsoft.Extensions.Logging.Abstractions.

At runtime, when the Project2 assembly is loaded, you will get “could not load file or assembly … Microsoft.Extensions.Logging.Abstractions 6.0.0.0”.

What is going on?

I will give you a hint: First, look at the consumer’s output directory, in particular the assembly version of the Microsoft.Extensions.Logging.Abstractions.dll.
Then look at both your package and the Microsoft.Extensions.Logging.Abstractions 6.0.1 package (using e. g. dotpeek or nuget package explorer).
Specifically, look at the dlls contained in Microsoft.Extensions.Logging.Abstractions 6.0.1 and their versions.
Now look at the dll references of the Project1 and Project2 dlls in your package.

Notice anything?

Different DLL versions for different TFMs

The Microsoft.Extensions.Logging.Abstractions 6.0.1 package contains two different dll versions, depending on the TFM of the consumer.

Consumer Project1 (TFM net48) will resolve 6.0.0.1 from the package and end up with a 6.0.0.1 assembly reference.
Consumer Project2 (TFM netstandard2.0) will resolve 6.0.0.0 from the package and end up with a 6.0.0.0 assembly reference.

Similar to this real-life scenario:

The package declares a package dependency on version 6.0.1
Assemblies in the same nuget package with different assembly reference versions

Because our fictional package consumer using Project1 and Project2 targets net48, it will resolve assembly version 6.0.0.1 from the declared package dependency on package version 6.0.1 and the build will copy that assembly into the consumer’s output directory.

Because there were no version conflicts visible to the build process, no binding redirects for the Microsoft.Extensions.Logging.Abstractions assembly are generated.
At runtime, when the Project1 assembly is loaded, its reference to assembly 6.0.0.1 can be resolved.
When the Project2 assembly is loaded, its reference to assembly 6.0.0.0 cannot be resolved.
The runtime will report an error “could not load file or assembly … Microsoft.Extensions.Logging.Abstractions 6.0.0.0” and stop the application.

Wow.

It gets even worse

Note that you do not even need two separate package references for this to occur.
It would be sufficient if only Project2 referenced Microsoft.Extensions.Logging.Abstractions package version 6.0.1 and Project1 referenced Project2.
Now, Project1 will (because of its TFM) end up with an assembly reference on Microsoft.Extensions.Logging.Abstractions version 6.0.0.1 through transitivity from Project2, even though the source of that transitive dependency (Project2) has a reference on assembly version 6.0.0.0.
Hilarity ensues.

A very specific workaround

In the particular case of Microsoft.Extensions.Logging.Abstractions we are lucky enough to have an easy workaround available.
Look at Microsoft.Extensions.Logging.Abstractions package version 6.0.0:

Same DLL versions for different TFMs

This package happens to use the same assembly versions for all relevant TFMs.
Downgrading Project1 and Project2 to Microsoft.Extensions.Logging.Abstractions package 6.0.0 will thus avoid the runtime error.

Note that this is not a solution to the general problem, but a workaround that happens to be available to us because of the assembly versions Microsoft happened to put into that particular package version 6.0.0.

If we do not have control over the problematic package but only the consumer, you can do two things.
I tested neither of them, but rather postulate that they work, based on the evidence above.

We could add an assembly redirect to our consumer’s config file.
This will be cumbersome in the future when we update the problematic package and new conflicts pop up.

We could also add a direct package reference to Microsoft.Extensions.Logging.Abstractions 6.0.1 to the consumer project.
That should cause the build to generate binding redirects up to assembly version 6.0.0.1.

What is the solution?

To avoid the issue altogether, we could just not use different TFMs within the same package.
To at least be warned, we could check at package build time if the resulting package has (within the same TFM folder) assemblies in it that have conflicting assembly reference versions, and then fail the build.

I will add a link to a gist that implements that check soon.