NewsDeveloper ToolsProgramming Languages

.NET 11 RC1: C# 15 Union Types, MAUI CoreCLR, What to Do

.NET 11 RC1 featured image showing C# 15 union types and MAUI CoreCLR migration concept
.NET 11 RC1 ships with C# 15 union types, MAUI CoreCLR migration, and runtime async improvements

.NET 11 RC1 landed September 8 with something RC releases rarely include: a go-live production license. You can deploy it today. C# 15 is the default language version, union types are out of preview, MAUI drops Mono on all mobile targets, and the async runtime gets a significant rework. Three weeks on, most teams are still sitting on .NET 10 LTS — that is probably the right call — but there are changes here worth understanding before the November GA.

C# 15 Union Types: Stable, Practical, and Long Overdue

Union types shipped as a preview in earlier .NET 11 builds. RC1 stabilizes them — no more <LangVersion>preview</LangVersion> in your project file. The feature does what every .NET developer has been wiring up manually with the OneOf NuGet package or custom Result<T> types: it lets you declare a type that is exactly one of a fixed set of cases, with compiler-enforced exhaustiveness.

public record OrderPlaced(Guid OrderId, decimal Total);
public record OutOfStock(string ProductCode, int Requested, int Available);

public union PlaceOrderResult(OrderPlaced, OutOfStock);

// Switch expression gets exhaustiveness checking
var message = result switch
{
    OrderPlaced placed => $"Order {placed.OrderId} confirmed",
    OutOfStock(var product, _, var available) => $"Only {available} of {product} in stock",
};

The compiler tells you when you miss a case — in a switch expression. That last clause matters: only switch expressions get exhaustiveness checking. Switch statements and if/else chains do not. There is also a default-value trap: an uninitialized union has Value == null, and a switch expression on it throws SwitchExpressionException at runtime with no compile-time warning. Handle it or your code will blow up in production.

JSON serialization is another sharp edge. Unions carry no type discriminator by default, so round-tripping through System.Text.Json requires explicit [JsonDerivedType] attributes on your API DTOs. If you are exposing unions over HTTP, plan for that before you ship. For the full picture on C# 15 union types, ABP’s deep dive covers the edge cases thoroughly.

Closed Class Hierarchies: When Unions Are Not Enough

The closed modifier ships alongside union types and is the right tool when you need reference-type semantics or want to share methods across cases. A closed hierarchy cannot be extended outside its declaring assembly — the compiler knows every possible subtype and enforces exhaustiveness in switch expressions.

public closed record OrderState;
public sealed record Draft : OrderState;
public sealed record Placed(DateTimeOffset PlacedAt) : OrderState;
public sealed record Paid(DateTimeOffset PlacedAt, string PaymentId) : OrderState;

Pick unions when your cases are unrelated types from different parts of the codebase. Pick closed hierarchies when the cases share behavior or need polymorphic dispatch. Both have the same versioning constraint: adding a case to a public type is a breaking change. Start with internal types before exposing either to an external API surface.

Runtime Async: The Numbers Are Real

This is the change most .NET 11 coverage buries. Since C# 5, async/await has worked by having the compiler generate state machine classes. In .NET 11, the JIT and runtime handle that transformation instead. The compiler emits a smaller IL contract; the runtime decides suspension points and continuation layout.

The performance results from opt-in testing are not incremental. On no-suspension async paths, allocation drops to zero bytes per operation — no Task objects, no Gen0 collections. ThreadPool and TaskCompletionSource continuation benchmarks show 3x–4x improvement. Deep async state-machine chains improve by 7.4x. Async methods also now participate in tiered compilation; previously they always ran tier-0 code, inflating allocations until the JIT warmed up. Microsoft’s full performance write-up has the detailed benchmarks.

The catch: runtime async is opt-in in .NET 11 via runtime-async=on and will not become the default until .NET 12. RC1 is the right time to enable it in a staging environment and check for behavioral differences. The feature is designed to be fully compatible, but verify in your app before relying on it.

MAUI CoreCLR: iOS Is Faster, Android Needs Caution

All MAUI mobile targets — Android, iOS, Mac Catalyst, tvOS — now default to CoreCLR. Mono is gone. The move brings tiered JIT, ReadyToRun precompilation, and Profile-Guided Optimization to mobile apps without code changes.

iOS and Mac Catalyst show clear startup wins. Android is more complicated: Microsoft’s baseline measurements put CoreCLR within 10% of Mono, but community reports document regressions in complex production apps — larger startup times and bigger binaries. Do not assume universal gains on Android. Measure your specific app before migrating production builds.

If you hit regressions, an escape hatch is available: add <UseMonoRuntime>true</UseMonoRuntime> to your project file. That option will disappear in a later release, but it buys time. Also note the hard cuts: Android x86 and Android API 23 and below are no longer supported. Check your minimum SDK targets before building. The official MAUI CoreCLR announcement has the full platform support matrix.

SDK: Mobile Testing and Reproducible Containers

dotnet test now runs test projects targeting Android, iOS, macOS, and Mac Catalyst — Microsoft ships new project templates for each. MAUI teams can finally run CI tests against real mobile targets without custom scripts. Container publishing now generates reproducible image digests when SOURCE_DATE_EPOCH is set and skips redundant layer uploads when the manifest already exists in the registry.

Should You Upgrade?

Honest answer: probably not yet for most production workloads. .NET 10 is an LTS release supported through November 2028. .NET 11 is an STS release with a shorter support window. The upgrade is straightforward — swap your target framework moniker — but the support math does not favor it unless you specifically need C# 15 union types, MAUI CoreCLR improvements, or want early access to runtime async.

Three cases where .NET 11 makes sense now: you are building a new MAUI app targeting iOS; you are writing a new service from scratch and want native union types instead of OneOf; or you are running high-throughput async infrastructure and can validate runtime async gains in staging. Everyone else: pin global.json to the .NET 10 SDK, watch the .NET blog for the GA post in November, and plan the migration then.

What to Do This Week

  • Install the .NET 11 RC1 SDK in a non-production environment and run your test suite
  • Enable runtime-async=on on one staging service and profile allocation differences
  • For MAUI: build a Release binary, test on physical Android devices, compare startup time against your .NET 10 baseline
  • Add CS8509 to WarningsAsErrors in Directory.Build.props — it flags non-exhaustive switch expressions before union types bite you
  • Pin the SDK version in global.json so CI does not silently pick up RC builds
ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:News