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 (asNewtonsoft.Json
does),dotnet publish --output outputfolder
will silently choose a single version of the DLLs to put in theoutputfolder
. 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.