.NET Core

.NET Core 3.0 brings many exciting new features, including a new major release of C#, improved performance and support for building Windows desktop applications (on Windows). In this article, we’ll look at interesting new features for Linux and Linux container users.

Faster and smaller SDK

The SDK is smaller and faster. Previous versions of .NET Core used NuGet packages when building an application. These NuGet packages contained both reference assemblies (describing the API) and implementation assemblies. .NET Core 3.0 uses references packs that come with the SDK. Because these packs don’t include an implementation, they are smaller than the NuGet packages. This makes the SDK smaller (in case NuGet packages were included with the SDK) or faster (in case NuGet packages had to be downloaded from nuget.org).

Source build .NET Core (like on Red Hat Enterprise Linux, Red Hat OpenShift, and Fedora .NET SIG) now includes the ASP.NET Core framework. This reduces time to build ASP.NET Core applications (no more nuget.org downloads), speeds up these applications (AOT compilation), and provides security fixes via package update (instead of having to rebuild the application).

Framework-dependent executables, single file publish, trimming

With previous versions, a native executable was only included when publishing a self-contained application. Now, a native executable is also included with framework-dependent applications:

$ dotnet new console -o console
$ cd console
$ dotnet publish
$ bin/Debug/netcoreapp3.0/publish/console
Hello World!

By default, this native executable is for the platform you are running on. It’s possible to build a native executable for a different platform (like Windows) by specifying a runtime id (-r).

$ dotnet publish -r win-x64 --no-self-contained
$ ls /tmp/console3/bin/Debug/netcoreapp3.0/win-x64/publish/
console3.deps.json  console3.dll  console3.exe  console3.pdb  console3.runtimeconfig.json

Note that we’re passing the --no-self-contained flag in order to build a framework-dependent application. Without it, the application would be self-contained (which means it includes the runtime).

If you want a native executable that works across a range of Linux distributions, you can specify linux-x64 as the rid. This executable depends on the GNU C library, which is used on many distributions (including Fedora and RHEL). If your executable is for a musl-based distribution, like Alpine, you can specify the linux-musl-x64 rid.

Both self-contained and framework-dependent application support packing the application into a single native executable. To do this, you can set the PublishSingleFile property.

$ dotnet publish -r win-x64 /p:PublishSingleFile=true
$ ls -lh bin/Debug/netcoreapp3.0/win-x64/publish/*.exe
-rwxrw-r--. 1 tmds tmds 66M Sep 13 09:08 bin/Debug/netcoreapp3.0/win-x64/publish/console.exe

The previous command packed the entire application into a self-contained Windows executable. You can see from the size (66M) that the runtime is included.

The SDK can now also leverage mono’s linker to detect unused assemblies. When we add the PublishTrimmed property, our self-contained app shrinks to 26M.

$ dotnet publish -r win-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true
$ ls -lh bin/Debug/netcoreapp3.0/win-x64/publish/*.exe
-rwxrw-r--. 1 tmds tmds 26M Sep 13 09:09 bin/Debug/netcoreapp3.0/win-x64/publish/console.exe


.NET Core adds support for Linux ARM64. The primary use case is Internet of Things (IoT) scenarios.

SerialPort support

The SerialPort class now also works on Linux. You can use this for example when running .NET Core on Raspberry Pi or other embedded Linux platforms.

TLS 1.3

When .NET Core runs on a system with OpenSSL 1.1.1 (like recent versions of Fedora, and RHEL8) SslStream and HttpClient use TLS 1.3 when the peer supports it. TLS 1.3 improves connection time and security.

Building systemd services

.NET Core comes with templates for building workers, which are long-running services. The worker template (dotnet new worker) has extension packages for building Windows services and systemd services.


The SDK now allows the application to be compiled ahead of time. This adds native code in the assemblies. It also means the code no longer has to be just-in-time compiled when it is first executed, which reduces startup time.

To publish a ready-to-run application, you need to specify a runtime identifier (which determines the native code you’ll generate) and the /p:PublishReadyToRun=true argument.

$ dotnet build -r linux-x64 /p:PublishReadyToRun=true

By default, this publishes a self-contained application. To publish a framework dependent application, you can add --no-self-contained.

OpenShift’s .NET Core builder (s2i-dotnetcore) can build ReadyToRun images by setting the new DOTNET_PUBLISH_READYTORUN to true.

GC in containers with low memory

.NET Core 3.0 works better in containers with low memory allocation. Previous versions allocated a large heap per CPU and performed garbage collections (GCs) based on memory used versus memory available. This could lead to the application going out-of-memory (OOM). .NET Core 3.0 takes into account the memory limits when heaps are created. This means the heaps are smaller, and the number of heaps is limited depending on the memory allocated to the container.

Changing ASP.NET Core applications to use workstation garbage collector (GC) has been a way to work around this issue (workstation GC uses a single, smaller heap). This workaround is no longer necessary with .NET Core 3.0.

If you wonder what type of GC your app is using: by default ASP.NET Core applications (which use the Microsoft.NET.Sdk.Web in the csproj file) use server GC. Console applications (Microsoft.NET.Sdk) default to workstation GC. When the application is running on a single processor (like a container with CPU allocated 1 or less), the runtime will automatically switch to workstation GC.

GC on machines with more than 64 CPUs

Windows APIs are based on processor groups of up to 64 processors, while Linux APIs work with an arbitrary number of processors. In previous versions of .NET Core, the GC would artificially group processors on Linux to form processor groups. By default, server GC would limit its number of threads to the number of processors in a single group (max 64). With .NET Core 3.0, the processor group emulation is removed from the GC, and server GC will match the number of processors allocated to the process (not limited to 64).

Huge page support

On systems that are configured with huge page support, the .NET GC can be configured to allocate huge pages by setting the environmental variable COMPlus_GCLargePages to 1. Because memory is reserved when the application starts the GC needs to know how much memory it can use. The runtime assumes some limits when running in a container. If you’re running outside a container, you need to provide these this using COMPlus_GCHeapHardLimit/COMPlus_GCHeapHardLimitPercent.

Diagnostic tools

With .NET Core 3.0, Microsoft is providing cross-platform command-line tools for diagnostics: dotnet-dump (for collecting and analyzing dumps), dotnet-trace (for collecting traces), and dotnet-counters (for live viewing of performance counters).

For example, the following commands show how to install dotnet-dump, use it to collect a dump from the running .NET Core application console, load the dump, and show a managed stack trace.

$ dotnet tool install -g dotnet-dump
$ dotnet dump collect -p $(pidof console)
Writing minidump with heap to /tmp/core_20190911_104217
$ dotnet dump analyze /tmp/core_20190911_104217                                                                                                                                                                                                                                                                
Loading core dump: /tmp/core_20190911_104217 ...
Ready to process analysis commands. Type 'help' to list available commands or 'help [command]' to get detailed help on a command.
Type 'quit' or 'exit' to exit the session.
> clrstack                                                                                                                                                                                                                                                                                                                   
OS Thread Id: 0x3f4f (0)
        Child SP               IP Call Site
00007FFE05DB3068 00007fe0f046c83c [HelperMethodFrame: 00007ffe05db3068] System.Threading.Thread.SleepInternal(Int32)
00007FFE05DB31B0 00007FE0760B580B System.Threading.Thread.Sleep(Int32)
00007FFE05DB31C0 00007FE0765C007D console.Program.Main(System.String[]) [/tmp/console/Program.cs @ 9]
00007FFE05DB34A8 00007fe0ef98df83 [GCFrame: 00007ffe05db34a8] 
00007FFE05DB39A0 00007fe0ef98df83 [GCFrame: 00007ffe05db39a0] 

As you can see, console is calling Thread.Sleep from Program.Main.


Here we covered several interesting features of .NET Core 3.0 on Linux and Linux containers. For a broader picture, check out the what’s new in .NET Core 3.0 documentation.

Last updated: March 29, 2023