A C# union -also called a discriminated union, tagged union or sum type- is a type whose value is exactly one of a fixed, closed set of case types. Switch over it and the compiler checks that every case is handled, with no default arm needed. C# 15 ships this as the union keyword.
In an August 2025 video, Mads Torgersen -lead designer for the C# Programming Language- outlined the C# team’s plans for C# unions. The team was aiming at potentially shipping this feature with the C# 15 release in November 2026. The video is here and he talks about union from 1:02:25 till 1:10:30.
Update April 2026: What was a possibility nine months ago is now real. Unions shipped as a preview in .NET 11 Preview 2. We have updated the post below to reflect what actually landed in the language. Most of the original plan went in unchanged. A few details ended up nicer than what Mads described in the video.
The short version:
- C# unions arrive with C# 15 / .NET 11. GA is expected November 2026, and a first preview already landed in .NET 11 Preview 2 (April 2026).
- The
unionkeyword composes existing types into a closed set. No shared base class or interface required. - Pattern matching is exhaustive. Cover every case type and the switch compiles with no
defaultarm. - Under the hood a union is a
record structholding oneobject? Value. Value types get boxed unless you hand-write the layout. - Unlike the
OneOfNuGet package, exhaustiveness is enforced by the compiler, not at runtime.
To try it today, still target net10.0 and set the language version to preview in the csproj:
|
1 |
<LangVersion>preview</LangVersion> |
As we will explain later you need also to append this code in your project:
|
1 2 3 4 5 6 7 8 9 |
namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } } |
C# Union Syntax: The Plan That Shipped
Mads originally introduced this code to illustrate the plan. The recently shipped syntax matches it exactly:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Pet pet = new Dog("Rover"); var description = pet switch { Dog(var name) => name, Cat(var adjective) => $"A {adjective} cat", Bird bird => $"A {bird.Species}", }; public union Pet (Dog, Cat, Bird); public record class Dog(string Name); public record struct Cat(string Adjective); public record class Bird(string Species); public record class Shark(int Teeth); |
This code feels very idiomatic to C#. Here are some remarks:
Petwraps the existing typesDog,Cat, andBird. Once declared, you no longer repeat the constituent types.Petalready carries that information.- The first line,
Pet pet = new Dog("Rover");, compiles because the compiler emits an implicit conversion from each case type to the union. The compiler also rejects any value of an unlisted type. SoPet pet = new Shark(20);fails to compile. - Unlike some languages such as F#, C# does not let you declare the case types inside the union. You declare them on their own. For comparison, in F# a union looks like the snippet below, where
Car,Truck, andBicyclebelong to theVehicleunion. They act as tags for the various cases rather than as types of their own. This is why functional languages use the names discriminated union or tag union. The C# team picked a different model, closer to a structural union or type union.
|
1 2 3 4 |
type Vehicle = | Car of model : string * year : int | Truck of model : string * year : int * payload : float | Bicycle of model : string |
- A key benefit shows up in pattern matching. The compiler knows
Petonly ever holds aDog,Cat, orBird. So aswitchexpression overPetcompiles without warning once all three arms are present. No_discard, nodefaultarm. That is the whole point of a closed set of types. - Extend the union later with
Shark, and the compiler warns everyswitchonPetthat misses the new case. This makesPeta closed union type. The compiler fixes its set of possibilities and enforces them. Class inheritance works the opposite way. You can add new subclasses without breaking existing code, thanks to polymorphism on virtual members. This is the Open-Close Principle (OCP): open for extension, closed for modification. In a word OCP applies to class inheritance but not to unions.
How C# Unions Work Behind the Scenes
The C# team turned union types into a record struct with a single read-only object? Value property. The design feels simple and powerful. In C# virtually any type fits into an object reference: class, struct, interface, delegate, enumeration, primitive types… but not unmanaged pointers for example.
|
1 2 3 4 5 6 7 8 9 10 11 |
#region public union Pet (Dog, Cat, Bird); [System.Runtime.CompilerServices.Union] public partial record struct Pet : IUnion { public Pet(Dog value) => Value = value; public Pet(Cat value) => Value = value; public Pet(Bird value) => Value = value; public object? Value { get; } } #endregion |
The Default Value and the Null Arm
A union is a struct, so it has a default value where the underlying Value is null. The compiler treats that as a real case you might have to handle:
|
1 2 3 4 5 6 7 8 9 |
Pet pet = default; var description = pet switch { Dog d => d.Name, Cat c => c.Adjective, Bird b => b.Species, null => "no pet", }; // description is "no pet" |
Consider a union whose case types are all non-nullable. The compiler won’t force you to write a null arm. It just warns the way it does for any other nullable. As soon as one case type is itself nullable (Bird?, int?, etc.), you must add a null arm for exhaustiveness.
The UnionAttribute and IUnion Contract
The UnionAttribute and the IUnion interface both live in System.Runtime.CompilerServices. .NET 11 Preview 4 does not yet ship them in the BCL. To play with the feature you have to declare them yourself:
|
1 2 3 4 5 6 7 8 9 10 11 |
namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] public sealed class UnionAttribute : Attribute; public interface IUnion { object? Value { get; } } } |
Later previews will bring them into the runtime. The important point: this is a public contract. Any type that carries [Union], exposes one single-parameter public constructor per case type, and implements IUnion behaves as a union. The compiler picks it up for pattern matching and exhaustiveness. In other words, the union keyword is sugar over a pattern that anyone can implement by hand. We will come back to that.
Adding a Body to a Union
A union declaration goes beyond its case list. It can also carry a body. That body is handy when you want helper methods that hide the switch from callers:
|
1 2 3 4 5 6 7 8 9 |
public union OneOrMore<T>(T, IEnumerable<T>) { public IEnumerable<T> AsEnumerable() => Value switch { T single => [single], IEnumerable<T> many => many, null => [] }; } |
A few things worth pointing out in that snippet:
- Unions can be generic.
OneOrMore<T>is a single declaration that works for anyT. - Inside the body, the compiler treats
Valueas maybe-null. That matches reality: a defaultOneOrMore<T>has a nullValue. - The combination “single item or collection of items” is a real pain point in API design. Until now the choices were ugly. Take
IEnumerable<T>everywhere and force callers to wrap. Or takeobjectand write defensive code. Or define overloads. WithOneOrMore<T>the API tells the truth about what it accepts. The helper hides the dispatch:
|
1 2 3 4 5 |
OneOrMore<string> singleTag = "dotnet"; OneOrMore<string> manyTags = new[] { "csharp", "unions", "preview" }; foreach (var tag in singleTag.AsEnumerable()) Console.Write($"[{tag}] "); // [dotnet] |
This kind of small union with a helper is, I suspect, where most real-world usage will happen. Not the textbook Pet example.
Could C# Unions be Smart for Value Type Only Unions?
Storing everything in an object? has a trade-off. The runtime boxes any value type listed in the union, such as Cat, the moment you write it into Value. The garbage collector then has to manage that box, which adds a small performance overhead.
A smarter implementation could optimize storage for value types that share the same memory footprint. The runtime could also allocate space based on the largest value type in the union. Smaller types would then carry padding overhead. Either approach would significantly complicate things. At 1:08:20 Mads explains that the C# team will let you define the layout of a value-type-only union by hand. That is exactly the door [Union] + IUnion opens. You write a custom record struct and tag it with [Union]. Inside, you store the cases as discriminated fields in an explicit layout, not as a boxed object?. The compiler still checks exhaustiveness on switches over your type. But you keep control of the allocation story.
HasValue and TryGetValue
To make this work the team adds two opt-in members the compiler recognises on a custom union: HasValue and a generic TryGetValue<T>(out T value). When both are present, the compiler uses them instead of going through Value. A switch then never boxes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public struct IntOrString : IUnion { private readonly int _kind; private readonly int _value1; private readonly string _value2; public IntOrString(int value) { _kind = 1; _value1 = value; } public IntOrString(string value) { _kind = 2; _value2 = value; } // still needs to exist for IUnion public object? Value => _kind switch { 1 => _value1, 2 => _value2, _ => null }; // access pattern that avoids boxing. public bool HasValue => _kind != 0; public bool TryGetValue(out int value) { if(_kind == 1) { value = _value1; return true; } value = -1; return false; } public bool TryGetValue(out string value) { if(_kind == 2) { value = _value2; return true; } value = ""; return false; } } |
I had wished for this kind of escape hatch in the original post. Good to see the team thought about it. I still believe unions made solely of value types form a common scenario. Think parser implementation, messaging protocol, financial instruments. Avoiding allocations there matters. In the same vein C# 13 eliminated object allocations for params collections.
Reading Bytes with MemoryMarshal
Modern C# and MemoryMarshal make it easy to cast a range of bytes into any value type instance. Handy when you build such a hand-written value-only union:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.Diagnostics; using System.Runtime.InteropServices; Span<byte> bytes8 = stackalloc byte[8] { 2, 0, 0, 0, 1, 0, 0, 0 }; // Convert first 4 bytes to uint uint value32 = MemoryMarshal.Read<uint>(bytes8); Debug.Assert(value32 == 2); // Convert all 8 bytes to ulong ulong value64 = MemoryMarshal.Read<ulong>(bytes8); Debug.Assert(value64 == 4294967298); // Convert all 8 bytes to MyStruct ref MyStruct myStruct = ref MemoryMarshal.AsRef<MyStruct>(bytes8); Debug.Assert(myStruct.X == 2); Debug.Assert(myStruct.Y == 1); [StructLayout(LayoutKind.Sequential)] struct MyStruct { public uint X; public uint Y; } |
What Is Not Yet in C# 15 Unions
The team flags a few items as not done yet:
- Union member providers. Today, if every case type in a
Petexposes aNameproperty, you still need a switch to read it. The team plans to let you writepet.Namedirectly when every case carries the property. Useful, and clearly inspired by F# active patterns and Kotlin sealed-class member access. - Uninitialized-field analysis. Today the compiler does not warn when you forget to assign a union field in a constructor. Even though
defaultmeans a nullValue. - Closed hierarchies and closed enums. Not part of the union feature itself. The team is openly considering a
closedmodifier on class hierarchies and a closed flavour ofenum. Both would give you the same exhaustiveness guarantee unions give. Useful where unions are not the right modelling tool.
Visual Studio support already lives in the Insiders build. Regular Visual Studio will get it in a future update.
C# Unions vs OneOf, Class Hierarchies and Enums
Before unions, C# developers reached for the OneOf NuGet package, an abstract base class, or an enum plus a payload. Here is how the native union compares:
| Approach | Closed set | Compiler exhaustiveness | No inheritance needed |
|---|---|---|---|
C# 15 union |
Yes | Yes | Yes |
OneOf<...> NuGet |
Yes | No (runtime) | Yes |
| Class / interface hierarchy | No (open) | No | No |
enum |
Yes | Partial | n/a |
The win is the middle column. Only the union keyword turns “did I handle every case?” into a compile-time check rather than a runtime habit.
C# Unions FAQ
What is a C# union type?
A union is a type whose value is exactly one of a fixed set of case types: classes, structs, records or primitives. The compiler enforces that set and checks that every switch handles all cases. Other names for the concept are discriminated union, tagged union and sum type.
When are C# unions released?
They are scheduled for C# 15 with .NET 11, GA expected November 2026. A first preview shipped in .NET 11 Preview 2 in April 2026, behind <LangVersion>preview</LangVersion>.
How do C# unions differ from F# discriminated unions?
F# declares the case tags inside the union. C# composes existing standalone types instead, so its model is closer to a structural or type union than to a tag union. Both give exhaustive pattern matching.
Do C# unions box value types?
Yes. The generated union stores its content in a single object? property, so a struct case is boxed. You avoid that by hand-writing a [Union] record struct with HasValue and TryGetValue members.
How do I use C# unions today?
Set the preview language version, then declare UnionAttribute and IUnion yourself in System.Runtime.CompilerServices until a later preview adds them to the BCL.
Are C# unions better than the OneOf library?
They cover the same need but at the language level. OneOf checks cases at runtime, while the union keyword gives compiler-enforced exhaustiveness and cleaner pattern matching.
Conclusion
The August 2025 prediction held up. Unions are coming in C# 15, and the syntax Mads showed on stage matches what shipped in Preview 2. Pattern matching ended up a touch nicer than expected, since you switch on the union directly rather than on its Value. Generic unions and union bodies turn the feature into something actually useful for everyday API design. The [Union] / IUnion escape hatch leaves room for the value-type-only optimisation that some of us were worried about.
The team also confirmed they will work with the .NET Core Base Class Library team. Many union types should ship alongside the language feature. The F# interop story remains open, though.
In 2024 we already wrote a blog post named C# Discriminated Union to explain the concept to C# developers: C# Discriminated Union: What’s Driving the C# Community’s Inquiries?. Hopefully, things are becoming clearer. We will keep updating this post as the feature evolves toward C# 15 GA in November 2026.
