.NET 11 Preview 3: A Developer's Review
.NET 11 hits its third preview about six months out from the planned November 2026 GA, and unlike Preview 1 and 2, which were mostly plumbing, Preview 3 is the one where you can actually feel the shape of the release.
Runtime Async stops being a "preview-feature gate" experiment, the JIT picks up another batch of free-money optimizations, ASP.NET Core gets Zstandard out of the box, and C# 15's union types - the language feature people have been asking about for the better part of a decade.
This isn't an LTS release, .NET 11 is the next Standard Term Support version, with 24 months of support, which already changes the calculus for whether you upgrade. Below is what's actually worth your attention as a developer, and where the rough edges still are.
C# 15 union types: the big one
If you've been writing OneOf<T1, T2> libraries, hand-rolling sealed record hierarchies, or just envying F# devs, this is the headline. C# 15 introduces a union keyword that declares a value is exactly one of a fixed set of types, with compiler-enforced exhaustiveness. Union types arrived in Preview 2; Preview 3 polishes IDE support around them.
The C# team's approach is closer to a type union than F# discriminated unions: the member types are existing types you've defined separately, not tag cases nested inside the union declaration. A union is essentially a struct that wraps an object and constrains what can go in it. From the call site, it feels native - implicit conversion from member types into the union, exhaustive switch expressions over Pet.Value, and no default arm needed when the compiler can see all cases.
[Union]
public partial struct Pet : IUnion
{
public Pet(Dog dog);
public Pet(Cat cat);
public Pet(Bird bird);
}
string Describe(Pet pet) => pet.Value switch
{
Dog d => $"Dog named {d.Name}",
Cat c => $"Cat ({c.Color})",
Bird b => $"Bird, {b.Species}",
// no default - compiler knows the set is closed
};
[Union]
public partial struct Pet : IUnion
{
public Pet(Dog dog);
public Pet(Cat cat);
public Pet(Bird bird);
}
string Describe(Pet pet) => pet.Value switch
{
Dog d => $"Dog named {d.Name}",
Cat c => $"Cat ({c.Color})",
Bird b => $"Bird, {b.Species}",
// no default - compiler knows the set is closed
};
<Union>
Public Partial Structure Pet
Implements IUnion
Public Sub New(dog As Dog)
End Sub
Public Sub New(cat As Cat)
End Sub
Public Sub New(bird As Bird)
End Sub
End Structure
Function Describe(pet As Pet) As String
Return pet.Value Select Case
Case d As Dog
Return $"Dog named {d.Name}"
Case c As Cat
Return $"Cat ({c.Color})"
Case b As Bird
Return $"Bird, {b.Species}"
' no default - compiler knows the set is closed
End Select
End FunctionThe pros are real: closed types, no invalid states, exhaustiveness at compile time instead of NotImplementedExceptionat 3am. Add a Shark to the union and every switch over Pet lights up with warnings until you handle it.
The cons are also real, and worth flagging in any honest review. The exposed object Value property is a smell - there's an open GitHub discussion about hiding it behind something type-safer. Public constructors implicitly define which types the union accepts, which is neither discoverable nor explicit. F# interop is unresolved (the two models are fundamentally different). And the broader exhaustiveness story still has gaps: closed hierarchies and closed enums, the two proposals that would complete the picture, are still proposals. Unions alone are great. Unions plus closed enums plus closed hierarchies would be a generational shift. We're not quite there.
Runtime Async V2 and JIT improvements
Runtime Async is .NET's quiet rewrite of how async/await actually executes. Instead of the C# compiler emitting a state-machine class per async method, the runtime itself manages suspension and resumption. The visible payoff: cleaner stack traces, smaller allocations, and a debugger that doesn't make you scroll past MoveNext frames to find your own code.
In Preview 3, Runtime Async drops the EnablePreviewFeatures requirement. You still flip the feature switch - <Features>runtime-async=on</Features> - but you no longer have to opt every API call into preview territory. NativeAOT and ReadyToRun support landed in this preview too, which closes the gap between JIT and AOT scenarios. Continuation objects get reused more aggressively, and locals that haven't changed aren't saved across suspension. In async-heavy code paths - think a Kestrel pipeline or an EF Core query worker - that's a meaningful drop in allocation pressure.
The JIT picked up its usual batch of "your existing code is now faster, do nothing":
- Multi-target switch expressions like x is 0 or 1 or 2 or 3 or 4 now fold into branchless checks.
- Bounds checks on values[^1] + values[^2] patterns get eliminated more aggressively, and the common i + cns < len case in loops collapses cleanly.
- Unsigned-int-to-float and unsigned-int-to-double casts are faster on pre-AVX-512 x86 hardware - niche, but real if you're on older boxes.
WebAssembly users get WebCIL payload loading directly in the runtime, better debug symbols, and float[] / Span<float> / ArraySegment<float> marshaling without the round-trip overhead. None of these are individually dramatic, but together they're the kind of compounding work that makes Blazor WASM feel less like a compromise.
The catch is the hardware floor. .NET 11 raises minimum instruction-set requirements on x86/x64 and Arm64. Apple Silicon and most Linux SBCs are fine - the ReadyToRun target on Arm64 just adds LSE - but very old x86 hardware is out. Audit your fleet before you assume an in-place upgrade.
ASP.NET Core and Blazor
Zstandard is the headliner here. ASP.NET Core now supports zstd for both response compression and request decompression, and it's enabled by default when you add the middleware. The configuration is the same shape you'd expect:
builder.Services.AddResponseCompression();
builder.Services.AddRequestDecompression();
builder.Services.Configure<ZstandardCompressionProviderOptions>(options =>
{
options.CompressionOptions = new ZstandardCompressionOptions { Quality = 6 };
});
builder.Services.AddResponseCompression();
builder.Services.AddRequestDecompression();
builder.Services.Configure<ZstandardCompressionProviderOptions>(options =>
{
options.CompressionOptions = new ZstandardCompressionOptions { Quality = 6 };
});
Imports Microsoft.Extensions.DependencyInjection
builder.Services.AddResponseCompression()
builder.Services.AddRequestDecompression()
builder.Services.Configure(Of ZstandardCompressionProviderOptions)(Sub(options)
options.CompressionOptions = New ZstandardCompressionOptions With {.Quality = 6}
End Sub)For APIs serving JSON or text payloads to clients that already speak zstd - increasingly common in mobile and gRPC-adjacent ecosystems - this is a measurable bandwidth win without taking a dependency on a third-party library. It's also, notably, a community contribution rather than a Microsoft-internal one, which is a healthy signal.
Blazor's Virtualize<TItem> finally stops assuming every row is the same height. This was a long-standing irritation: any list with variable content - comments, chat messages, anything with wrapped text - needed manual workarounds. Now the component measures items at runtime. The Preview 3 release also fixes a clutch of Blazor bugs: a null reference in Virtualize, scroll-container detection with overflow-x, the Web Worker template in published WASM apps, TempData lazy loading edge cases, and a IJSObjectReference leak in ResourceCollectionProvider. Individually small, collectively the kind of cleanup that signals the framework is maturing past the "new thing every release" phase.
Kestrel also starts processing HTTP/3 requests without waiting for the control stream and SETTINGS frame, which trims first-request latency on new connections. If you've been measuring HTTP/3 P99s and seeing odd cold-start tails, this is your fix.
.NET MAUI
MAUI in Preview 3 is mostly about closing gaps that have made it feel like a beta for too long. The Map control gets pin clustering, custom pin icons, custom JSON styling, and click events on circles, polygons, and polylines - all things that real production map UIs need and that previously required custom handlers per platform. A built-in LongPressGestureRecognizer is now available without rolling your own. Implicit XAML namespace declarations are on by default, which trims boilerplate at the top of every file.
Platform parity gets a bump: Permissions.PostNotifications is now implemented on iOS (it had been Android-only), and Android picks up preview support for Android 17 and API level 37.
The honest assessment: this is steady, sensible iteration, not a reinvention. MAUI in 2026 is in a much better place than MAUI in 2023, but if you walked away from it earlier in its life, Preview 3 alone isn't going to drag you back. If you're already on MAUI, these are exactly the QoL changes you want.
SDK, CLI, and .NET watch
This is the section where the small things add up. A few that I think genuinely change daily workflow:
.NET sln can now create and edit solution filters (.slnf) directly from the CLI. For monorepos and large Microsoft-style solutions, opening a 200-project SLN to work on three of them has been a real cost. Now you can scope from the terminal:
dotnet new slnf --name MyApp.slnf
dotnet sln MyApp.slnf add src/Lib/Lib.csprojdotnet new slnf --name MyApp.slnf
dotnet sln MyApp.slnf add src/Lib/Lib.csprojFile-based apps (the .NET run app.cs workflow) finally support #:include, which means C# scripts can split helpers into separate files. Combined with editor completion for the directive in Roslyn, this nudges file-based apps from "toy" to "viable for real automation tools" - the niche PowerShell and small Python scripts have owned for years.
.NET run -e FOO=BAR lets you pass environment variables on the command line without exporting shell state or editing launch profiles. Tiny, but if you've ever had three terminals open with different ASPNETCORE_ENVIRONMENT values, you know the pain.
.NET watch integrates with Aspire app hosts, auto-relaunches after a crash on the next file change, and handles Ctrl+C more gracefully for WinForms and WPF (a perpetual papercut). .NET format accepts --framework for multi-targeted projects. .NET test in MTP mode supports --artifacts-path. And .NET tool exec / dnx no longer prompts for an extra approval, which had been a friction point for one-off tool runs.
The pain points
A balanced review owes the rough edges, and Preview 3 has them.
The tooling story is rough. Visual Studio 2026 is still in preview six months after .NET 10 shipped, and Microsoft-hosted build agents don't have stable VS 2026 support yet. A change in a .NET 10 SDK patch release required MSBuild 18 (VS 2026), which is a clean violation of the semver guarantees Microsoft promotes. Anyone running CI on Microsoft-hosted agents has had to either pin SDK 10.0.4 or switch to preview build images. If you're considering moving CI pipelines to .NET 11 previews, expect more of the same - preview SDKs have, by the team's own admission, broken things in 10.0.2 and 10.0.3 before stabilizing.
Runtime Async is still opt-in. Even with the preview-features gate gone, you have to flip runtime-async=on. That's fine for greenfield code; for libraries shipped on NuGet, you still can't assume your consumers have the switch on, so the practical benefits are deferred until the feature defaults on (not in .NET 11).
Hardware requirement bumps. The minimum x86/x64 instruction-set requirements moved up. Most teams won't notice. Some will - and they'll find out at deploy time if they don't audit first.
STS, not LTS. .NET 11 is supported for 24 months. .NET 10, the current LTS, gets 36 months. For shops on slow upgrade cadences, .NET 10 is still the more conservative choice, and adopting .NET 11 means committing to another upgrade in 2028. The case for adopting an STS is the features; the case against is the calendar.
Preview means preview. This isn't a stability rant - Microsoft's preview process has been good - but Preview 3 is not a release candidate. Production deployments wait for RC1 at the earliest. Internal tools, side projects, and exploration are the right scope right now.
Verdict
If you write C# every day, .NET 11 Preview 3 is worth installing and playing with this week - particularly to get hands on union types, which are the most consequential language change in years. If you maintain libraries, the JIT and Runtime Async work means your code is going to get faster on .NET 11 with no edits, which is the best kind of upgrade. If you ship MAUI apps, the Map and gesture work is real progress.
If you run production .NET workloads, the answer is the boring one: keep planning, keep watching, and bookmark the GA in November. The pieces that excite are landing, but the tooling chain - VS, build agents, and the SDK patch cadence - is where the friction actually lives, and it has not been solved yet.
| --- |
Sources: .NET Blog announcement, What's new in .NET 11 (Microsoft Learn), Runtime release notes, ASP.NET Core release notes, SDK release notes, Explore union types in C# 15.
