Skip to content
  • English Deutsch

benjamin-abt.com

Performance is a feature | Cloud - AI - Azure - .NET - Sustainability

  • Home
  • Blog
  • Portfolio
    • Overview
    • Workshops & Training
    • Publications
  • Contact
  • Home
  • Blog

C# 15 Unions: Unions are finally in .NET

C# 15 Unions: Unions are finally in .NET
March 9, 2026 7 min read BEN ABT
.NET Csharp .NET 11 Language Design Open Source

After many years of workarounds, design discussions and library-level substitutes, unions are finally becoming a first-class part of C#. The proposal in the C# language repository is no longer a distant idea. With the feature now appearing in the .NET 11 preview train as an early C# 15 capability, the direction is concrete enough to discuss in practical terms.

This matters because unions solve a modeling problem that has existed in C# for a very long time: a value often has exactly one of several valid shapes, but the type system has not had a native way to express that closed set. The result has usually been some combination of nullable values, exception-driven control flow, boolean flags and hand-written result wrappers.

Unions change that. At the same time, there is no reason to wait for C# 15 before writing union-oriented code. Unio already brings that style of modeling to .NET 8, .NET 9 and .NET 10 with very low overhead and code that starts with Unio today is already close to the shape that native unions will want tomorrow.

What the proposal actually adds

The important detail is that the proposal is broader than a single syntax form. It defines a union model for the language and then offers a concise declaration syntax on top of that model.

At a high level, the proposal introduces four capabilities that matter in day-to-day code:

  • Implicit conversions from case types into the union type
  • Pattern matching that unwraps the union contents automatically
  • Exhaustiveness checking in switch expressions when all case types are covered
  • Improved nullability flow for union contents

The headline syntax is straightforward:

C#
public union Pet(Cat, Dog, Bird);

That declaration means Pet can hold exactly one of those case types. The compiler then allows direct construction through the case values and treats pattern matching over Pet as operating on a closed set.

C#
 1public sealed record Cat(string Name);
 2public sealed record Dog(string Name);
 3public sealed record Bird(string Name);
 4
 5public union Pet(Cat, Dog, Bird);
 6
 7public static string Describe(Pet pet) =>
 8    pet switch
 9    {
10        Cat cat => $"Cat: {cat.Name}",
11        Dog dog => $"Dog: {dog.Name}",
12        Bird bird => $"Bird: {bird.Name}"
13    };

No fallback branch is required here, because the union is understood as exhaustive once all case types have been handled.

Union types and union declarations are not the same thing

One of the most important parts of the proposal is easy to miss on a quick read: C# is not only adding a union ... declaration form. It is also defining what a union type is.

According to the proposal, any class or struct marked with System.Runtime.CompilerServices.UnionAttribute can participate in union behavior, as long as it exposes the required public members. In other words, the language is being taught a pattern, not just a keyword.

That distinction is significant for two reasons.

First, it means unions are designed as a language capability that can extend beyond compiler-generated declarations. Existing or hand-written types can opt into the model.

Second, it leaves room for specialized implementations. The shorthand declaration is intentionally opinionated, but the language model is broader than that shorthand.

The generated union declaration is intentionally opinionated

The proposal is very explicit about the default representation chosen for union declarations. A union declaration lowers to a plain struct with a single object-typed Value property and a generated constructor for each case type.

That has two immediate consequences.

  • It is compact.
  • Value-type cases are boxed when stored through the default declaration form.

This is not an accident. The proposal positions union declarations as the simple, broadly useful default. For many application-level cases, that is the right trade-off. Service results, command outcomes, parsing states and protocol messages are often dominated by clarity and correctness, not by the last few nanoseconds of storage efficiency.

Still, the proposal also acknowledges that this default will not be ideal for every scenario. It explicitly leaves room for custom unions and for non-boxing access patterns where layout and performance matter more than a compact one-field representation.

That nuance is important. Native unions will be a major improvement for mainstream code, but they do not make specialized union libraries obsolete. They mostly replace the need to hand-roll the common case.

Why unions matter in ordinary C# code

The immediate value of unions is not fashion and not syntax. It is the ability to state, in the type system, that an operation has a closed set of valid outcomes.

Consider a service that loads an order. In many codebases, the return path ends up as Order?, an exception on missing data or a custom wrapper that nobody can inspect without opening the implementation. A union makes the contract visible where it belongs: on the signature.

C#
1public sealed record NotFound;
2public sealed record Unauthorized;
3public sealed record Order(int Id);
4
5public union GetOrderResult(Order, NotFound, Unauthorized);

That signature documents three outcomes and no others. The compiler can then enforce that all three are handled. This is where unions become more than a convenience feature. They move correctness checks earlier, reduce hidden control flow and make result modeling much harder to misuse.

This is also the reason unions fit naturally beside other upcoming C# work such as closed hierarchies and case declarations. The language is moving toward a stronger notion of closed sets and exhaustiveness and unions are one of the clearest expressions of that direction.

Unio already brings this model to .NET 8 and newer

The practical question is whether it makes sense to adopt the union style now or wait until the C# 15 syntax is fully available. In many cases, waiting is unnecessary.

Unio already offers discriminated unions today on net8.0, net9.0 and net10.0. More importantly, it does so with a design that is intentionally lightweight at runtime:

  • A readonly struct core type
  • Typed generic fields instead of storing everything through object
  • A one-byte discriminator
  • Exhaustive Match and Switch APIs
  • A source generator for named union types

That design keeps the runtime overhead very small and avoids the boxing cost that object-based union storage introduces for value types. For applications that care about hot-path efficiency, that difference is not academic.

The basic usage is already close to the native mental model:

C#
 1using Unio;
 2
 3[GenerateUnio]
 4public sealed partial class GetOrderResult : UnioBase<Order, NotFound, Unauthorized>;
 5
 6public static string Describe(GetOrderResult result) =>
 7    result.Match(
 8        static Order order => $"Order {order.Id}",
 9        static NotFound _ => "Not found",
10        static Unauthorized _ => "Unauthorized");

This is not just a stopgap. It already captures the essential architectural choice that unions encourage: result types are explicit, closed and compiler-guided.

Starting with Unio keeps the eventual migration small

This is the most practical part of the story.

If code already models outcomes with explicit case types, returns named union result types and consumes them through exhaustive matching, then the eventual move to native unions in C# 15 is largely a surface migration.

The domain types stay the same:

C#
1public sealed record NotFound;
2public sealed record Unauthorized;
3public sealed record Order(int Id);

The Unio declaration used today:

C#
1using Unio;
2
3[GenerateUnio]
4public sealed partial class GetOrderResult : UnioBase<Order, NotFound, Unauthorized>;

can later move to a native union declaration:

C#
public union GetOrderResult(Order, NotFound, Unauthorized);

The consuming code then shifts from Match or Switch calls to native switch expressions and pattern matching, but the actual business modeling does not need to be reinvented.

In other words, starting with Unio today does not lead into a dead end. It trains a codebase toward the exact modeling discipline that native unions want: explicit case types, closed outcomes and exhaustive handling.

There is one important caveat and it should be stated plainly. Minimal migration effort does not automatically mean identical runtime behavior in every performance-sensitive path. Unio’s core type stores values in typed fields and avoids boxing. The default C# 15 union declaration is object-based and intentionally compact. For application service results, this difference will often be irrelevant. For performance-critical, value-heavy unions, replacement should be benchmarked rather than assumed.

That caveat does not weaken the overall conclusion. It simply draws the correct line between source-level migration cost and runtime storage characteristics.

A good moment for result types in C#

For a long time, unions in C# were discussed as something other languages had and C# lacked. That is no longer the right framing. The interesting part now is not whether C# should have unions, but how codebases can start taking advantage of the model immediately.

The C# 15 proposal finally gives the language a first-class answer for closed result shapes and exhaustive handling. Unio already provides a practical answer for current production code on modern .NET versions, with minimal overhead and a migration path that is mostly about syntax rather than architecture.

That combination is unusually strong. The language direction is now clear and the tooling to write union-oriented code today already exists.

Related articles

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026 - 11 min read

Containerizing .NET in 2026 Without a Dockerfile

Most .NET Dockerfiles repeat the same five steps: restore, publish, select the matching Microsoft runtime image, copy the output and start …

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026 - 12 min read

Problem Details in ASP.NET Core and .NET 11

builder.Services.AddProblemDetails() registers infrastructure. It does not create an error contract. An ASP.NET Core request can fail during …

Resilience in .NET with Microsoft.Extensions.Resilience

Sep 21, 2026 - 11 min read

Resilience in .NET with Microsoft.Extensions.Resilience

Retries are the most visible resilience strategy and the easiest one to misuse. Repeating a failing call can recover from a short network …


BEN ABT

BEN ABT

Ben is Chief Engineering Officer and Principal Software Engineer at Medialesson.de, with a strong focus on .NET and Microsoft Azure. In his professional role, he designs and builds highly scalable platforms for hybrid and cloud-based scenarios. He also advises companies and C-levels on migrating to the sovereign cloud.

He runs myCSharp.de, the largest and most active German-speaking C# community forum, actively contributes to both his own and external open-source projects and regularly shares his knowledge through his blog, at community events, conferences and as a writer for technical magazines.

Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms

Modernization

Training & Consulting

Contact Me
X Facebook LinkedIn WhatsApp
Back Next Blog Post

Open for work!

Contact Me
BEN ABT BEN ABT Software Engineer · Microsoft MVP Passionate about .NET, Azure, performance optimization and sustainable software engineering.

Medialesson GmbH Logo

Microsoft MVP Logo

Recent posts

View all
Stop Using nginx as Your Default Static File Server in Docker

Stop Using nginx as Your Default Static File Server in …

Oct 24, 2026
Containerizing .NET in 2026 Without a Dockerfile

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026
Problem Details in ASP.NET Core and .NET 11

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026
Resilience in .NET with Microsoft.Extensions.Resilience

Resilience in .NET with Microsoft.Extensions.Resilience

Sep 21, 2026
What Matters in ASP.NET Core 11

What Matters in ASP.NET Core 11

Sep 14, 2026

Tags

.NET (192) Csharp (96) ASP.NET Core (48) Azure (41) DevOps (17) Docker (14) Microsoft (14) EF Core (11) Microsoft MVP (11) AI (10) GitHub (10) GitHub Copilot (10) ASP.NET (9) Security (9) Synology (9) Architecture (8) Raspberry (8) Azure Functions (7) Cloudflare (7) Bicep (6)
benjamin-abt.com

About BEN

Benjamin Abt is a Software Engineer, Platform Architect and Microsoft MVP specializing in .NET, Azure, cloud architecture, performance optimization and sustainable software development.

Connect

  • Follow me on GitHub
  • Follow me on LinkedIn
  • Follow me on Mastodon
  • Microsoft MVP profile
  • Follow SchwabenCode on GitHub
  • Follow me on X

Quick Links

  • Portfolio
  • Blog
  • Workshops & Training
  • Publications
  • Get in Touch
  • Sitemap
Open for Work

© Copyright Benjamin Abt & SchwabenCode | 2011 - 2026

Content licensed under CC BY 4.0 unless otherwise noted.

  • Impressum
  • Privacy
  • Contact