An overview of key .NET 6 features

| 8 min. (1576 words)

.NET 6 is finally here, giving us a new long term stable version of .NET Core. .NET 6 succeeds .NET 5, which was generally seen as a “skip version” by most of us, getting limited use compared to .NET Core 3.1. With this release, we get updates to both the runtime and the C# language. In this post, we’re taking a closer look at what we see as three of the most useful .NET 6 features.

.NET

Many people updating to use 6 will be coming from 3.1 rather than 5, so this release will provide a number of improvements to both the runtime and the supporting ecosystem of tools (e.g. dotnet).

Performance improvements in .NET 6

Since its initial release, one of the big focuses for .NET Core has been improving performance, and .NET 6 continues this trend with over 500 performance-focused PRs.

Improving the speed and efficiency of both JIT and AOT compilation are key areas in this update. Improved inlining and de-virtualizing (identifying the concrete callsite for a virtually dispatched method at compile time) provide solid gains.

A big one for us has been the improved thread pool for sync heavy workloads. There is an order of magnitude improvement on how the thread pool scales the number of threads when it identifies that the existing threads are being blocked for too long. The thread pool itself has also been ported to pure managed code - another validation of how performant the runtime is these days.

Lastly, performance of mundane operations like DateTime.UtcNow have been a focus of a number of the PRs. You might think such a simple operation would already be very tightly optimized, but that’s not always the case. So basic common library functions like retrieving the next random number from a System.Random or initializing a new GUID also see 2x order speedups coming from .NET 5 to .NET 6.

If you’re interested in reading (a lot) more about this, including links to the underlying PRs, check out Stephen Toub’s labor of love post on this topic.

Hot Reload

Now, this surely must be one of the best .NET 6 features, given all the contention about locking it down to Visual Studio users only. It doesn’t disappoint, and brings back some of the rapid development fondly remembered by old-timers with Visual Basic 6, allowing you to modify source code at runtime and have that instantly reflected (e.g. the next time the method is invoked).

Previously we had dotnet watch, which would monitor the source code and rebuild an application when a change was detected. For larger applications, the build time becomes a bit of a drag, particularly for a point change in a specific function. In essence this is no different from the usual workflow of making changes, compiling your code and then seeing those changes apply. Hot Reload takes this concept one step further to limit the scope of recompilation down to the methods that were actually affected by the change.

The main focus for this improvement is web development, and Hot Reload covers both ASP.NET server side code and also Blazor server and client side code. Productivity improvements like this are always very welcome!

Single file bundling

For deploying .NET applications, you still need the runtime available. Asking users to install an updated runtime (like .NET 6) often proves a barrier to entry. In earlier releases, you could compile a single file output which includes the .NET framework as well as any dependencies in a single executable output for easier deployment - this has been useful for us. .NET 6 extends this coverage to allow single file outputs to be built for Windows and also for OS X as well.

C#

C# 10 provides a set of language improvements that deliver some additional syntactic sugar to reduce the amount of code, and improve the ability for expression. Sadly, a couple of key improvements I was hoping to see (namely null parameter checking and required parameters) didn’t make the cut, but should be coming down the line in C# 11.

Let’s have a look at what we did get:

Implicit usings, global usings and file scoped namespace declaration

Hello World in C# is now down to just 1 line:

Console.WriteLine("Hello World").

Previously you would need to wrap it in at least a method called Main, which could act as the entry point for the program and similarly a class as well. To avoid fully qualifying out methods we would normally bring into scope System with a using statement.

C# 10 brings a few changes that help remove some of that boilerplate code. First off, implicit usings are a compiler convenience, where the namespaces you need to import are helpfully identified and scoped for you. I’m a more verbose kind of developer myself, so I prefer to understand exactly how something was scoped. So personally, I’m seeing more benefit in one of the other improvements of C# 10: the ability to import at the global scope, avoiding the need to include using definitions on a per file basis.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

we can now have a single file containing the following global usings declared and all other files automatically have those available in scope:

global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text;
global using System.Threading.Tasks;

Speaking of namespaces, the last subtle improvement which will help reduce indentation in your code is the file scoped namespace declaration. Namespaces are a standard scoping mechanism in .NET, but the standard way involves wrapping the associated code in a namespace scope and indenting accordingly. Now we can take this:

namespace MyNextBigApplication
{
  internal class Token
  {
	public Token(string code) => Code = code;
	public static DateTime ClaimTime => DateTime.Now;
	public string Code { get; }
  }
}

and instead declare the namespace as a single line concern at the top of the file and then the rest of the file will be scoped under that namespace:

namespace MyNextBigApplication;

internal class Token
{
  public Token(string code) => Code = code;
  public static DateTime ClaimTime => DateTime.Now;
  public string Code { get; }
}

For most files, which are only ever in a single namespace scope, this helps remove untold numbers of spaces (or tabs).

Improvements to Lambdas

Lambdas have been a key language feature in C# since .NET 3, and C# 10 provides some additional niceties.

You can now directly specify the return type of a Lambda, rather than it being inferred, which provides qualification to help resolve overloaded method situations or to correctly scope the return type.

Consider the following:

var result = (string s) => 1;

The return type will be assumed to be Int32. However, if we wanted to return an Int16 we could do:

var result = (string s) => (short)1.

Or we can now explicitly define the return type, which helps with compile time checking and avoids runtime casting errors:

var result = short (string s) => 1;

The implicit resolution has been improved as well.

The second improvement is a bit more controversial, since the actual readability on this one is not an improvement. However, the functional improvement makes up for it, and brings parity between anonymous Lambda functions and standard methods for applying attributes:

var lambda = [Example(123)] (string s) => 1;

An example of where this becomes useful is with the minimal HTTP API changes that are also in .NET 6. You can set up a minimal HTTP server where the handlers are Lambda based but can still be decorated with routing attributes, e.g.

[HttpPost("/")] Alert CreateAlert(Alert a) => { do-stuff(); return a; };

Attributes can also be applied to parameters, though that seems a bit more of a niche case to me.

Record structs

In C# 9, we gained access to a new reference type - the record and also immutable properties (via init). In C# 10, this is now extended to cover structs as well. There are two key benefits to using records over standard classes or structs, which are to give value-based equality checking between instances and to support immutable structures.

To declare a struct as a record you specify:

public record struct Foo {}

To avoid confusion with a standard class-based record, you can also now use an alternate declaration of:

public record class Foo {}

as opposed to

public record Foo {}

Like with class-based records you can also use with-expressions to quickly clone an existing record and make specific changes.

public record struct Foo {}

Using the immutable record approach provides for a very strong performance gain over standard mutable structs and should be considered when optimizing for performance.

You can see a full list of the language improvements over on GitHub. You can also see what proposals are still pending for future versions of C#.


So there you have it — a pretty solid set of updates. If you’re on .NET Core or .NET 5, you’ll want to look at upgrading and the migration path is fairly minimal. Visual Studio 2022 is also out today, and finally gives us a 64-bit version of Visual Studio as well as its own set of performance wins to boost your productivity. If you’re using Raygun you will find our crash reporting provider already all set for .NET 6, and we’re in the process of releasing updated support for .NET 6 for APM, alongside support for profiling .NET applications deployed on Linux.

Happy coding!