﻿# What's new in \.NET 11?

\.NET 11 is out now, meaning it's time to start moving to it\. But first, let's see whether the new features in C\# 15, such as Runtime Async and JIT optimizations, justify an immediate upgrade\.

![1425_dotnet11/image1.png](https://import.viva64.com/docx/blog/1425_dotnet11/image1.png)

\.NET 11 focuses on performance and runtime improvements\. The release is a Standard Term Support \(STS\) release, so it gets two years of support\. Starting with \.NET 9, STS releases get 24 months of support instead of 18\.

This article covers the most notable changes to the runtime, libraries, and SDK\. For the complete list, see [the official documentation](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview)\.

## C\# 15

All of the C\# 15 features were covered in a [separate article](https://pvs-studio.com/en/blog/posts/csharp/1421/)\. The language gained union types, closed hierarchies, and extension indexers, and its approach to memory safety has been reworked\. This year's changes are genuinely significant\. What's your take on the new version?

## Performance

Every year, Steven Taub publishes a detailed review of the performance improvements in the new \.NET, walking through each change in depth\.

This year's review covers the JIT, Runtime Async, devirtualization, bounds checking, and a good deal more\.

For a thorough look at this part of \.NET 11, see Steven Taub's [article](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/)\.

## Libraries

### Compression

The [`System.IO.Compression`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression?view=net-11.0) namespace now includes Zstandard compression, through [`ZstandardStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.zstandardstream?view=net-11.0), [`ZstandardEncoder`](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.zstandardencoder?view=net-11.0), and related types\. When reading ZIP archives, the CRC32 checksum is now verified, so a corrupted archive throws an `InvalidDataException`\. ZIP entries now also support passwords and can be opened with the `FileAccess` option\. Deflate, ZLib, and GZip encoders and decoders gained a `Reset()` method, which lets you clear an instance's state and reuse it instead of allocating a new object\.

### LINQ

The new version improves the existing `Join` methods and adds [`FullJoin`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.fulljoin?view=net-11.0), which returns every element from both sequences\. Elements are matched by key, and unmatched elements on either side are kept with no partner\. Let's see `FullJoin` in action:

```cpp
var products = new List<(string Name, string? Category)>
{
  ("Laptop", "Electronics"),
  ("Table", null)
};
var categories = new List<(string Name, string Description)>
{
  ("Electronics", "Electronic devices"),
  ("Furniture", "Office furniture")
};

var joined = products.FullJoin(categories, p => p.Category, c => c.Name);
foreach (var (product, category) in joined)
    Console.WriteLine($"{product.Name ?? "(none)"} 
    : {category.Description ?? "(none)"}");
```

The code prints the following:

```cpp

Laptop: Electronic devices
Table: (none)
(none): Office furniture
```

In addition, LINQ now lets you call `Join` and `GroupJoin` without a `resultSelector`, so they return tuples directly, just as `LeftJoin`, `RightJoin`, and `FullJoin` do\.

### JSON

The [`System.Text.Json`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-11.0) namespace gained several capabilities\. For the new `union` type introduced in C\# 15, the serialization and deserialization methods were adapted so that the current value is written directly, with no wrapper or discriminator\. A JSON Lines \(JSONL\) output format is also available, where each value sits on its own line\. To do this, the new `JsonSerializer.SerializeAsyncEnumerable` method has a `topLevelValues` parameter\.

The [`JsonPolymorphicAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute?view=net-11.0) has a new property, [`InferClosedTypePolymorphism`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpolymorphicattribute.inferclosedtypepolymorphism?view=net-11.0), which turns on automatic inference of derived types for a single closed hierarchy without changing the serializer's global settings\. Previously, every derived type had to be registered explicitly with the `JsonDerivedType` attribute on the base type\. The setting can also be applied globally, through `JsonSerializerOptions` or the source generator options\. In \.NET 11, it looks like this:

```cpp
[JsonPolymorphic(InferClosedTypePolymorphism = true)]
closed record Shape;
record Circle(double Radius) : Shape;
record Square(double Length) : Shape;
```

Generic `GetTypeInfo<T>()` and  `TryGetTypeInfo<T>()` methods were added, both returning `JsonTypeInfo<T>`\. The release also brings the built\-in [`JsonNamingPolicy.PascalCase`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonnamingpolicy.pascalcase?view=net-11.0) policy, the `[JsonNamingPolicy]` attribute for types and their members, the option to apply `[JsonIgnore]` to an entire type at once, and built\-in converters for `BFloat16`, `Decimal32`, `Decimal64`, and `Decimal128`\.

### Asynchronous validation

Asynchronous validation is now available through [`AsyncValidationAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.asyncvalidationattribute?view=net-11.0) and [`IAsyncValidatableObject`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.iasyncvalidatableobject?view=net-11.0), along with new methods such as [`Validator.ValidateObjectAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator.validateobjectasync?view=net-11.0), [`Validator.TryValidateObjectAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator.tryvalidateobjectasync?view=net-11.0), and others\. It helps when a check has to query a database or an external service without blocking the thread\. `Microsoft.Extensions.Options` can validate options asynchronously too, including at application startup through the new [`IAsyncStartupValidator`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.options.iasyncstartupvalidator?view=net-11.0) interface\.

### New stream types

Four new `Stream` types have arrived: [`ReadOnlyMemoryStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream?view=net-11.0), [`WritableMemoryStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.writablememorystream?view=net-11.0), [`ReadOnlySequenceStream`](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequencestream?view=net-11.0), and [`StringStream`](https://learn.microsoft.com/en-us/dotnet/api/system.io.stringstream?view=net-11.0)\. They let you hand an existing buffer or string to any API that accepts a `Stream` without copying it first, which means fewer allocations and less pressure on the garbage collector\.

### Numeric types and parsing

[`System.Numerics`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics?view=net-11.0) gets  [`Decimal32`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal32?view=net-11.0), [`Decimal64`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal64?view=net-11.0), and [`Decimal128`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.decimal128?view=net-11.0) types\. These are IEEE 754\-2019 decimal floating\-point types with 7, 16, and 34 significant digits, respectively\. Like [`System.Decimal`](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?view=net-11.0), they store numbers in base 10, so `0.1` is represented exactly\. Unlike `System.Decimal`, however, they follow the standard, supporting NaN, infinity, and generic math\. They suit cases where IEEE 754 behavior matters, such as interoperating with other languages and databases that use the format\. A generic [`Complex<T>`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.complex-1?view=net-11.0) also arrived, which works with `float`, `Half`, `BFloat16`, and the new decimal types\.

A new method, [`INumberBase<TSelf>.TryParsePartial`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumberbase-1.tryparsepartial?view=net-11.0), which parses a number up to the first character that isn't part of it\. Previously, input like `123;789` made `TryParse` return `false`, and leave the result at  0\. Here is how it behaves now:

```cpp
public static void ParseTest()
{
  double.TryParsePartial("123;789", 
                          System.Globalization.NumberStyles.Number, 
                          null, out var result, out var consumed);
  Console.WriteLine($"Parsed value (TryParsePartial): {result}");
  Console.WriteLine($"Consumed: {consumed}");

  double.TryParse("456;032", null, out result);
  Console.WriteLine($"Parsed value (TryParse): {result}");
}
```

The output:

```cpp
Parsed value (TryParsePartial): 123
Consumed: 3
Parsed value (TryParse): 0
```

So when something gets in the way, such as a wrong delimiter or a stray character, you now get a partial result along with the count of characters read\. 

### Processes

The [`Process`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=net-11.0) class now has a whole set of new APIs\. Previously, running a utility and collecting its output meant subscribing to `OutputDataReceived` and `ErrorDataReceived` or reading the streams by hand\. Now a single [call](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0) does it:

```cpp
ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", 
                           ["status", "--porcelain"]);

Console.WriteLine(result.StandardOutput);
Console.WriteLine($"Exit code: {result.ExitStatus.ExitCode}");
```

Three more methods came with it:

* [`Process.StartAndForget`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0) starts a process without waiting for it to finish\.
* [`ProcessStartInfo.StartSuspended`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.startsuspended?view=net-11.0) starts a process without waiting for it to finish\.
* [`SafeProcessHandle.Resume()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.safehandles.safeprocesshandle.resume?view=net-11.0) resumes a suspended process\.

## Runtime

### Runtime Async

This is a major change to the runtime this year\. For every `async` method, the compiler generates a state machine, whereas [Runtime Async](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#runtime-async) hands that work to the runtime itself\. The feature started as an experiment in \.NET 10\. In \.NET 11 it's still marked as a preview, but \.NET 11 projects no longer need `EnablePreviewFeatures` to use it\. The \.NET libraries are already compiled with Runtime Async and contain no generated state machines\. To enable it in your project, add this to the project file:

```cpp
<PropertyGroup>
  <Features>runtime-async=on</Features>
</PropertyGroup>
```

The gains come in two areas\. First, overhead drops\. The JIT now optimizes `Task.FromResult`, `Task.CompletedTask`, and `ValueTask.FromResult` into faster paths, reuses continuation objects, and applies tiered compilation to hot asynchronous methods\. Second, the call stack obtained from `new StackTrace()` is now easier to read, since it shows only the actual call chain, excluding internal calls\. Debugging improves as well, partly because stepping over an `await` no longer takes you into infrastructure code\.

### New processor requirements

This [change](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#updated-minimum-hardware-requirements) may stop the app from launching on older hardware\. On x86/x64, the minimum level for JIT and NativeAOT is now `x86-64-v2` instead of `x86-64-v1`\. On a processor that falls short, the application simply won't run and will report that the basic instruction sets are missing\.

The ReadyToRun images for Windows and Linux on x64 now target `x86-64-v3` \(`AVX2`, `BMI2`, `FMA`, and others\)\. On processors without these instructions, the application still starts, but the precompiled code has to be recompiled by the JIT, which slows startup\. Processors like that are hard to find these days, but it's worth checking in advance which hardware your project runs on\.

### JIT enhancements

Besides supporting Runtime Async, the [JIT](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#jit-improvements) has picked up several useful tricks\. It now removes more bounds checks, including those for expressions like `values[^1]` and `i + const < length`, as well as for `span[0]` after the `!span.IsEmpty` check\. It can evaluate `string.Equals` and `SequenceEqual` at compile time when both operands are known\. It also strips redundant conditions and branches, and emits a `MUL` instruction for `Math.BigMul` on x64\.

### NativeAOT and ReadyToRun

In [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#jit-improvements), interface method calls now go through a shared dispatch helper, which then redirects them to the appropriate implementation\. Call sites get smaller, and code that works heavily with interfaces runs faster\.

`Comparer<T>.Default` and `EqualityComparer<T>.Default` are now specialized during precompilation in [ReadyToRun](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime#readytorun-improvements) during precompilation, so they no longer need the JIT at runtime\.

## SDK

### The CLI is now faster

The `dotnet` [CLI](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/sdk#cli-architecture) runs as a NativeAOT application by default\. Commands such as `dotnet --version`, `dotnet --info`, and `dotnet --help` run entirely natively, while external tools, such as `dotnet ef` launch directly, skipping the managed part of the CLI\.

The number of SDK installers for Linux and macOS has dropped: identical builds are now replaced by symbolic links to a single copy\.

### Single\-file applications

The ability to run \.cs files without a project, introduced in \.NET 10, keeps evolving\. The [`#:include`](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps#include) directive lets you to spread code across several files, or even reference a pre\-built DLL\.

These applications also support `dotnet format` now\. Restarting an unmodified NativeAOT application reuses the output compiled earlier\.

### The dotnet run and dotnet watch commands

The `dotnet run` command can pass environment variables to the application straight from the command line using the `-e` parameter\.

The `dotnet watch` command gained integration with the Aspire host, automatic restart after a crash, and device selection for MAUI projects\.

### Testing

The [`dotnet test`](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/sdk#dotnet-test-improvements) command in Microsoft\.Testing\.Platform now comes with a lot of new options\. For example, you can cap the run time or stop after a set number of failed tests\.

A new `--no-dependencies` option skips building dependent projects\. There's also an experimental mode that runs only the tests affected by your changes\.

## Conclusion

\.NET 11 is, above all, a runtime release\. Runtime Async is already in use inside the \.NET libraries, the JIT has grown smarter, and the CLI starts faster\. In everyday work, though, the features you'll notice most are the C\# 15 additions and smaller but handy methods such as [`Process.RunAndCaptureTextAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0), [`FullJoin`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.fulljoin?view=net-11.0), and [`TryParsePartial`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumberbase-1.tryparsepartial?view=net-11.0)\.

If you're planning to upgrade, check the new processor requirements first\.

The [Microsoft documentation](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-11/overview) has the complete list of changes\. Which new features did you like? Let us know in the comments\.

For more articles on C\# and \.NET, visit the [PVS\-Studio blog](https://pvs-studio.com/en/blog/posts/csharp/), and [subscribe here](https://pvs-studio.com/en/subscribe/) to catch new ones as they come out\.