The dangers of using the '--output' parameter with 'dotnet publish'

TL;DR: Using dotnet publish with the --output parameter on a solution publishes all projects in that solution into a single output repository - any files with conflicting names will silently overwrite each other. If any projects depend on different versions of a NuGet package and the DLLs of the NuGet package have identical file names across its versions (as Newtonsoft.Json does), dotnet publish --output outputfolder will silently choose a single version of the DLLs to put in the outputfolder. The projects depending on any other versions of the DLls will throw an exception at runtime. The solution: Publish each project individually into project-specific folders. This is not an issue on Ubuntu for reasons I have not yet uncovered

Did you ever encounter something like this?

Unhandled exception. System.IO.FileLoadException:
Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'.
The located assembly's manifest definition does not match the assembly reference. (0x80131040)
File name: 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'

The exception means that Newtonsoft.Json is used by some piece of code, but, when the application tried loading Newtonsoft.Json, it found an unexpected version of it. In this concrete example, the code was looking for Newtonsoft.Json version 12 but found another version of it.

After some digging, I found out how I get myself into this mess: I created a trap for dotnet publish. The exception has nothing to do with Newtonsoft.Json and has everything to do with dotnet publish.

Let’s see why that is.

Laying the trap for dotnet publish

Creating a small code base that will make dotnet publish output the exception-throwing code is easy. All it takes is a solution with two projects that reference different versions of a NuGet package:

# Create a new solution
dotnet new sln -n MySolution
# Create a new console application
dotnet new console --name MyConsoleApp
dotnet sln add MyConsoleApp/MyConsoleApp.csproj
# Create another new console application
dotnet new console --name MyOtherConsoleApp
dotnet sln add MyOtherConsoleApp/MyOtherConsoleApp.csproj

# Add *different* versions of a NuGet package to the projects
dotnet add MyConsoleApp/MyConsoleApp.csproj \
  package Newtonsoft.Json --version 12.0.3
dotnet add MyOtherConsoleApp/MyOtherConsoleApp.csproj \
  package Newtonsoft.Json --version 11.0.2

As .NET only loads dependencies if they are actually used in the code, I need to modify Program.cs in each console application.

The modification is simple: I introduce a variable s that contains a new instance of JsonSerializerSettings from Newtonsoft.Json, making .NET dynamically load the the assembly.

The two console applications now look like this:

MyConsoleApp/Program.cs:

namespace MyConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello world from my console app!");
      var s = new JsonSerializerSettings();
      Console.WriteLine("Still alive!");
    }
  }
}

MyOtherConsoleApp/Program.cs

namespace MyOtherConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello world from my OTHER console app!");
      var s = new JsonSerializerSettings();
      Console.WriteLine("Still alive!");
    }
  }
}

The trap has been placed.

Pushing dotnet publish into the trap

Now for the final touch; the command that will mess things up in a constellation like this: dotnet publish --output <some-output-folder>.

Let’s run it and see why it messes stuff up.

dotnet publish -o publish

The entire solution has now been published into the publish folder - let’s run the console applications.

First, we enter the folder where everything has been published:

cd publish/

Now we’ll run MyOtherConsoleApp:

➜  dotnet MyOtherConsoleApp.dll
Hello world from my OTHER console app!
Still alive!!

It works! Awesome, let’s try MyConsoleApp:

➜  dotnet MyConsoleApp.dll
Unhandled exception. System.IO.FileLoadException:
Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'.
The located assembly's manifest definition does not match the assembly reference. (0x80131040)
File name: 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'

It crashed :’(

Looking at the publish folder, the culprit might be obvious:

➜  ls
MyConsoleApp.deps.json
MyConsoleApp.runtimeconfig.json
MyOtherConsoleApp.pdb
MyConsoleApp.dll
MyOtherConsoleApp.deps.json
MyOtherConsoleApp.runtimeconfig.json
MyConsoleApp.pdb
MyOtherConsoleApp.dll
Newtonsoft.Json.dll

There is only a single version of Newtonsoft.Json.dll, but MyConsoleApp and MyOtherConsoleApp depends on two different versions. All of a sudden, the exception makes sense: Where MyOtherConsoleApp can find exactly what it needs, MyConsoleApp only finds a file with the correct name but the wrong contents.

What can we do to solve it?

The solution: Publish the projects you need, not the entire solution

The solution is simple: Avoiding publishing the entire solution into a single folder. For my specific use case, I didn’t actually need the entire solution but two projects of a solution with many.

Let’s try that on the sample code.

From the solution’s root directory, I can publish the two console applications individually as such:

dotnet publish MyConsoleApp/MyConsoleApp.csproj -o publish/MyConsoleApp
dotnet publish MyOtherConsoleApp/MyOtherConsoleApp.csproj -o publish/MyOtherConsoleApp

Let’s run the published applications now:

➜  dotnet publish/MyConsoleApp/MyConsoleApp.dll
Hello world from my console app!
Still alive!
➜  dotnet publish/MyOtherConsoleApp/MyOtherConsoleApp.dll
Hello world from my OTHER console app!
Still alive!

The applications now run as they should - problem solved.