Dotnet Core 3.0 deployment models
Published: 2020-10-20
Starting with .NET Core 3.0, we have two deployment models for publishing .NET applications: Framework-dependent and Self-contained.
Framework-dependent deployment was the traditional approach. With this model, the .NET runtime must already be installed on the machine where the application runs. This approach has several benefits:
- Generates smaller deployment packages.
- Supports cross-platform deployment, provided the application is published for a compatible target.
- Allows multiple applications to share the same runtime installation.
- Makes runtime upgrades easier because the runtime can be updated independently of the application.
Self-contained deployment allows you to publish your application together with the .NET runtime and its dependencies. The target machine does not need a separate .NET runtime installation.
Because the deployment package includes the runtime, you will typically find hundreds of files in the publish folder. Naturally, this makes the deployment package considerably larger than a framework-dependent deployment.
.NET Core 3.0 also introduced several publishing options that help optimize deployment packages:
- Produce single file
- Enable ReadyToRun compilation
- Trim unused assemblies
Let’s briefly look at ReadyToRun compilation before exploring the other two options.
ReadyToRun compilation performs ahead-of-time (AOT) compilation of application assemblies. This reduces the amount of JIT compilation required when the application starts, potentially improving startup performance. Some JIT compilation may still be required at runtime.
Richard Lander explains this feature in detail in his article: Announcing .NET Core 3.0 Preview 6.
The following table summarizes the availability of these publishing options in the .NET Core 3.1 tooling used for this experiment. Actual support and compatibility depend on the SDK, target runtime, and application configuration.
| Deployment model | Produce single file | Enable ReadyToRun | Trim unused assemblies |
|---|---|---|---|
| Framework-dependent | Yes | Yes | Not supported in the original .NET Core 3.1 publishing workflow |
| Self-contained | Yes | Yes | Yes |
I’ll walk you through these options one by one and verify their impact on the published application.
For this experiment, I’ll create a simple console application that prints Hello World to the console.
Application details
- Application name:
HelloWorld - .NET version: .NET Core 3.1
- Development and publishing environment: Visual Studio 2019
- External dependency:
Newtonsoft.Json
I’ll use Newtonsoft.Json as an example of an external dependency that isn’t part of the .NET runtime.
I assume you already know how to publish a .NET application to a folder using Visual Studio. If not, Microsoft’s documentation on publishing with Visual Studio is a good starting point.
Let’s start with the Framework-dependent deployment model.
1. Framework-dependent deployment

FRAMEWORK-DEPENDENT (NO FILE PUBLISH OPTIONS)
First, I published the application without selecting any of the File Publish Options.
Fact check:
Let’s examine the publish folder.

As you can see, the publish folder contains just 6 files, with a total size of approximately 853 KB.
The deployment package is relatively small because the .NET runtime is not included. The application relies on the runtime installed on the target machine.
FRAMEWORK-DEPENDENT (PRODUCE SINGLE FILE)
Next, I selected the Produce single file option and published the application again.
Fact check:

This time, the publish folder contains just 2 files, while the total size remains approximately 853 KB.
The main benefit here is a reduction in the number of files that need to be deployed. The overall deployment size remains practically unchanged because the runtime is still not included.
Now let’s move on to the second deployment model.
2. Self-contained deployment
With self-contained deployment, the application carries the .NET runtime along with it. This means the target machine doesn’t need a separate .NET runtime installation.

SELF-CONTAINED (NO FILE PUBLISH OPTIONS)
Initially, I left all three File Publish Options unchecked. As usual, I deleted the entire bin directory before publishing to ensure that the results were based on a fresh build.
Fact check:

As you can see, the publish folder contains 226 files, with a total size of approximately 67 MB.
That’s considerably larger than the framework-dependent deployment. However, this is expected because the publish folder now includes the .NET runtime and its dependencies.
Notice that I’ve highlighted two assemblies: System.IO.dll and Newtonsoft.Json.dll. We’ll compare these assemblies with the output of subsequent publishing configurations to understand what changes.
SELF-CONTAINED (PRODUCE SINGLE FILE)
This time, I selected the Produce single file option and published the application again.
Fact check:

There are now just 2 files, and the total size is approximately 66.7 MB.
Almost the entire deployment package is contained in the executable, with only a small additional file remaining.
Notice that single-file publishing primarily changes how the application is packaged. It doesn’t significantly reduce the overall deployment size because the runtime and application dependencies still need to be included.
SELF-CONTAINED (TRIM UNUSED ASSEMBLIES)
Next, I’ll disable Produce single file and enable Trim unused assemblies.
Fact check:

This time, the publish folder contains 112 files, and the total size has come down to approximately 35.2 MB.
That’s a substantial reduction of almost 50% compared with the original self-contained deployment.
If you compare these files with the output from the self-contained deployment without publishing options, you’ll notice that System.IO.dll and several other assemblies are no longer present.
This is the result of assembly trimming. The .NET tooling analyzes the application and removes code that it determines is not required by the application.
One interesting observation is that Newtonsoft.Json.dll is still present, even though our sample application doesn’t actually use it.
This is because trimming is not simply a process of deleting every unused NuGet package or dependency. The trimmer analyzes application code and assemblies, and its ability to remove unused code depends on how those dependencies are implemented and whether they support trimming.
SELF-CONTAINED (PRODUCE SINGLE FILE AND TRIM UNUSED ASSEMBLIES)
Finally, let’s enable both Produce single file and Trim unused assemblies.
Fact check:

As expected, the trimming process reduces the application and runtime payload by removing code that is not required. Single-file publishing then packages the application and its dependencies into a single executable.
This gives us the benefit of both optimizations: a smaller deployment package with fewer files to manage.
3. Tidbits
A few additional things worth knowing:
-
Self-contained deployments are platform-specific. When publishing a self-contained application, you must specify a target runtime identifier (RID), such as
win-x64orlinux-x64. The runtime is included for that particular platform and architecture. You can still target multiple platforms, but you need to publish a separate deployment for each target. -
Single-file publishing has runtime and hosting considerations. In .NET Core 3.1, single-file publishing had limitations, including restrictions around IIS hosting. Don’t assume that a single-file executable can be deployed in every hosting environment without additional configuration. These limitations have evolved in later .NET releases.
-
You can further reduce the size of .NET executables using Warp. I covered this in another article: Warp It!.
Conclusion
The right deployment model depends on your application and deployment environment.
- Choose Framework-dependent when you want smaller deployment packages and can manage the .NET runtime on target machines.
- Choose Self-contained when you want to deploy applications without requiring a separate runtime installation.
- Use Produce single file when you want to simplify deployment by reducing the number of files.
- Use Trim unused assemblies when you want to reduce deployment size and your application and dependencies are compatible with trimming.
- Consider ReadyToRun when startup performance is important and the additional publishing cost and package size are acceptable.
These options can be combined to achieve a deployment package that balances size, startup performance, and deployment simplicity.