Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// <BodyMembers>
public union OneOrMore<T>(T, IEnumerable<T>) where T : notnull
public record class Meters(double Value);
public record class Feet(double Value);

public union Length(Meters, Feet)
{
public IEnumerable<T> AsEnumerable() => this switch
public double TotalMeters => this switch
{
T single => [single],
IEnumerable<T> multiple => multiple
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};

public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
// </BodyMembers>

Expand All @@ -19,11 +25,14 @@ public static void Run()
// <BodyMembersExample>
static void BodyMembersExample()
{
OneOrMore<string> single = "hello";
OneOrMore<string> multiple = new[] { "a", "b", "c" }.AsEnumerable();
Length distance = new Meters(10.0);
Length height = new Feet(3.0);

Console.WriteLine(distance.TotalMeters); // output: 10
Console.WriteLine(height.TotalMeters); // output: 0.9144

Console.WriteLine(string.Join(", ", single.AsEnumerable())); // output: hello
Console.WriteLine(string.Join(", ", multiple.AsEnumerable())); // output: a, b, c
Length total = distance.Add(height);
Console.WriteLine(total.TotalMeters); // output: 10.9144
}
// </BodyMembersExample>
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
// Uncomment when union member providers are available in the compiler:

// <MemberProvider>
// [System.Runtime.CompilerServices.Union]
// public record class Outcome<T> : Outcome<T>.IUnionMembers
// {
// private readonly object? _value;
//
// private Outcome(object? value) => _value = value;
//
// public interface IUnionMembers
// {
// static Outcome<T> Create(T? value) => new(value);
// static Outcome<T> Create(Exception? value) => new(value);
// object? Value { get; }
// }
//
// object? IUnionMembers.Value => _value;
// }
[System.Runtime.CompilerServices.Union]
public record class Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;

private Outcome(object? value) => _value = value;

public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
}

object? IUnionMembers.Value => _value;
}
// </MemberProvider>

// <MemberProviderExample>
// public static class MemberProviderScenario
// {
// public static void Run()
// {
// Outcome<string> ok = "success";
// var msg = ok switch
// {
// string s => $"OK: {s}",
// Exception e => $"Error: {e.Message}",
// };
// Console.WriteLine(msg);
// }
// }
public static class MemberProviderScenario
{
public static void Run()
{
Outcome<string> ok = "success";

Check failure on line 25 in docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs

View workflow job for this annotation

GitHub Actions / snippets-build

D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\MemberProvider.cs(25,30): error CS0029: Cannot implicitly convert type 'string' to 'Outcome<string>' [D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\unions.csproj]
var msg = ok switch
{
string s => $"OK: {s}",

Check failure on line 28 in docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs

View workflow job for this annotation

GitHub Actions / snippets-build

D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\MemberProvider.cs(28,13): error CS8121: An expression of type 'Outcome<string>' cannot be handled by a pattern of type 'string'. [D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\unions.csproj]

Check failure on line 28 in docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs

View workflow job for this annotation

GitHub Actions / snippets-build

D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\MemberProvider.cs(28,13): error CS0656: Missing compiler required member 'Outcome<string>.Value' [D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\unions.csproj]
Exception e => $"Error: {e.Message}",

Check failure on line 29 in docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs

View workflow job for this annotation

GitHub Actions / snippets-build

D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\MemberProvider.cs(29,13): error CS8121: An expression of type 'Outcome<string>' cannot be handled by a pattern of type 'Exception'. [D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\unions.csproj]

Check failure on line 29 in docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs

View workflow job for this annotation

GitHub Actions / snippets-build

D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\MemberProvider.cs(29,13): error CS0656: Missing compiler required member 'Outcome<string>.Value' [D:\a\docs\docs\docs\csharp\language-reference\builtin-types\snippets\unions\unions.csproj]
};
Console.WriteLine(msg);
}
}
// </MemberProviderExample>
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
ManualUnionScenario.Run();
NonBoxingAccessScenario.Run();
ClassUnionScenario.Run();
// Uncomment when union member providers are available in the compiler:
// MemberProviderScenario.Run();
MemberProviderScenario.Run();

This file was deleted.

58 changes: 33 additions & 25 deletions docs/csharp/language-reference/builtin-types/union.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Union types"
description: Learn about union types in C#. Unions express values from a closed set of types with exhaustive pattern matching support.
ms.date: 03/20/2026
ms.date: 06/05/2026
f1_keywords:
- "union_CSharpKeyword"
helpviewer_keywords:
Expand Down Expand Up @@ -32,9 +32,6 @@ A union differs from other type declarations in important ways:
- Unlike an `interface`, a union is closed—you define the complete list of case types in the declaration, and the compiler uses that list for exhaustiveness checks.
- Unlike a `record`, a union doesn't add equality, cloning, or deconstruction behavior. A union focuses on "which case is it?" rather than "what fields does it have?"

> [!IMPORTANT]
> In .NET 11 Preview 2, the runtime doesn't include the `UnionAttribute` and `IUnion` interface. To use union types, you must declare them yourself. To see the required declarations, see [Union implementation](#union-implementation).

## Union declarations

A union declaration specifies a name and a list of case types:
Expand All @@ -51,40 +48,43 @@ public union Pet(Cat, Dog, Bird);

When a case type is a value type (like `int`), the value is boxed when stored in the union's `Value` property. Unions store their contents as a single `object?` reference.

A union declaration can include a body with additional members, just like a struct, subject to some restrictions. Union declarations can't include instance fields, auto-properties, or field-like events. You also can't declare public constructors with a single parameter, because the compiler generates those constructors as union creation members:
A union declaration can include a body with additional members, just like a struct, subject to some restrictions. Union declarations can't include instance fields, auto-properties, or field-like events. You also can't declare public constructors with a single parameter, because the compiler generates those constructors as union creation members. The following `Length` union adds a `TotalMeters` property that uses pattern matching to handle every case type, along with an `Add` method that combines two lengths:

:::code language="csharp" source="snippets/unions/BodyMembers.cs" id="BodyMembers":::

## Union conversions

An implicit *union conversion* exists from each case type to the union type. You don't need to call a constructor explicitly:
An implicit *union conversion* exists from each case type to the union type:

:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="BasicConversion":::

Union conversions work by calling the corresponding generated constructor. If a user-defined implicit conversion operator exists for the same type, the user-defined operator takes priority over the union conversion. For details on conversion priority, see the [language specification](~/_csharplang/proposals/unions.md).
Union conversions work by calling the corresponding generated constructor. If a user-defined implicit conversion operator exists for the same type, the user-defined operator takes priority over the union conversion. If more than one case type is equally applicable to the source value, the union conversion is ambiguous, and the compiler reports an error. For details on conversion priority, see the [feature specification](~/_csharplang/proposals/unions.md).

A union conversion to a nullable union struct (`T?`) also works when `T` is a union type:

:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullableUnionExample":::

## Union matching
## Union pattern matching

When you pattern match on a union type, patterns apply to the union's `Value` property, not the union value itself. This "unwrapping" behavior means the union is transparent to pattern matching:
When you pattern match on a union type, patterns generally apply to the union's `Value` property, not the union value itself. This "unwrapping" behavior means the union is transparent to pattern matching:

:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching":::

Two patterns are exceptions to this rule: the `var` pattern and the discard `_` pattern apply to the union value itself, not its `Value` property. Use `var` to capture the union value when `GetPet()` returns a `Pet?` (`Nullable<Pet>`):
Three patterns are exceptions to this rule: the discard `_` pattern, the `var` pattern, and the `not` pattern apply to the union value itself, not its `Value` property. Use `var` to capture the union value when `GetPet()` returns a `Pet?` (`Nullable<Pet>`):

```csharp
if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
```

In logical patterns, each branch follows the unwrapping rule individually. The following pattern tests that the `Pet?` isn't null *and* its `Value` isn't null:
In logical patterns, each branch follows the unwrapping rule individually. The left branch of an `and` pattern can change the incoming value that the right branch sees. Because the `not` pattern applies to the incoming union value rather than its `Value`, a leading `not null` doesn't unwrap the value for the branch that follows it:

```csharp
GetPet() switch
{
var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks Value
// 'var pet' captures the Pet?; 'not null' applies to the Pet? value (not pet.Value)
var pet and not null => ...,
// 'not null' doesn't unwrap to Pet, so 'var value' still captures the Pet?
not null and var value => ...,
}
```

Expand All @@ -111,34 +111,37 @@ For nullable union struct types (`Pet?`), `null` succeeds when the nullable wrap

## Union exhaustiveness

A `switch` expression is exhaustive when it handles all case types of a union. The compiler warns only if a case type isn't handled. You don't need to include a discard pattern (`_`) or `var` pattern to match any type:
A `switch` expression is exhaustive when it handles all case types of a union. The compiler warns only if a case type isn't handled. You don't need to include a discard pattern (`_`) or `var` pattern to match any type when the expression is definitely assigned:

:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching":::

If the null state of the union's `Value` property is "maybe null," you must also handle `null` to avoid a warning:

:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullHandling":::

This situation can arise when the `union` expression is the default value, or isn't definitely assigned, as shown in the preceding sample.

## Nullability

The compiler tracks the null state of a union's `Value` property through the following rules:

- The default null state of a union's `Value` property is "maybe null" if the default null state of any case type is "maybe null." Otherwise, the default null state is "not null."
- When you create a union value from a case type (through a constructor or union conversion), `Value` gets the null state of the incoming value.
- When the non-boxing access pattern's `HasValue` or `TryGetValue(...)` members query the union's contents, the null state of `Value` becomes "not null" on the `true` branch.

## Custom union types

The compiler converts a `union` declaration to a `struct` declaration. The struct is marked with the `[System.Runtime.CompilerServices.Union]` attribute, implements the `IUnion` interface. It includes a public constructor and an implicit conversion for each case type along with a `Value` property. That generated form is opinionated. It's always a struct, always boxes value-type cases, and always stores contents as `object?`.
The compiler converts a `union` declaration to a `struct` declaration. The struct is marked with the `[System.Runtime.CompilerServices.Union]` attribute and implements the `IUnion` interface. It includes a public constructor and an implicit conversion for each case type along with a `Value` property. That generated form is opinionated. It's always a struct, always boxes value-type cases, and always stores contents as `object?`.

When you need different behavior - such as a class-based union, a custom storage strategy, interop support, or if you want to adapt an existing type - you can create a union type manually.
You might need different behavior if you want to adapt an existing type, create a class-based union, use a custom storage strategy, or need interop support. You can create a union type manually.

Any class or struct with a `[Union]` attribute is a *union type* if it follows the *basic union pattern*. The basic union pattern requires:

- A `[Union]` <!--<xref:System.Runtime.CompilerServices.UnionAttribute>--> attribute on the type.
- One or more public constructors, each with a single by-value or `in` parameter. The parameter type of each constructor defines a *case type*.
- A public `Value` property of type `object?` (or `object`) with a `get` accessor.

All union members must be public. The compiler uses these members to implement union conversions, pattern matching, and exhaustiveness checks. You can also implement the [non-boxing access pattern](#non-boxing-access-pattern) or create a [class-based union type](#class-based-union-types).
All the preceding union members must be public. The compiler uses these members to implement union conversions, pattern matching, and exhaustiveness checks. You can also implement the [non-boxing access pattern](#non-boxing-access-pattern) or create a [class-based union type](#class-based-union-types). Your custom union type can add additional members.

The compiler assumes that custom union types satisfy these behavioral rules:

Expand All @@ -158,24 +161,21 @@ The following example shows a custom union type:
A custom union type can optionally implement the *non-boxing access pattern* to enable strongly typed access to value-type cases without boxing during pattern matching. This pattern requires:

- A `HasValue` property of type `bool` that returns `true` when `Value` isn't `null`.
- A `TryGetValue` method for each case type that returns `bool` and delivers the value through an `out` parameter.
- A `TryGetValue` method for each case type that returns `bool` and delivers the value through an `out` parameter. `TryGetValue` returns `true` only when `Value` is a non-null value of that case type. The `out` parameter's type is identity-convertible to the case type, or to the underlying value type when the case type is a nullable value type.

:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingAccessPattern":::

:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingExample":::

The compiler prefers `TryGetValue` over the `Value` property when implementing pattern matching, which avoids boxing value types.

<!-- Union member providers aren't yet available in .NET 11 Preview 2. Uncomment the following section when the feature ships. -->
<!--
### Union member providers

A union type can delegate its union members to a nested `IUnionMembers` interface. When this interface is present, the compiler looks for `Create` factory methods instead of constructors:

:::code language="csharp" source="snippets/unions/MemberProvider.cs" id="MemberProvider":::

Union member providers are useful when the union type needs a private constructor or when the creation logic requires a factory pattern, such as with `record class` union types.
-->

### Class-based union types

Expand All @@ -189,9 +189,20 @@ For class-based unions, the `null` pattern matches both a null reference and a n

## Union implementation

The following attribute and interface support union types at compile time and runtime:
<!-- TODO: Replace with xrefs and remove the copied code when Preview 5 API ref is available. -->
Union types rely on the `UnionAttribute` and `IUnion` types in the `System.Runtime.CompilerServices` namespace. The runtime includes these types beginning with .NET 11 Preview 5:

:::code language="csharp" source="snippets/unions/RuntimePolyfill.cs" id="RuntimePolyfill":::
```csharp
namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;

Comment thread
BillWagner marked this conversation as resolved.
public interface IUnion
{
object? Value { get; }
}
```

Union declarations generated by the compiler implement `IUnion` <!--<xref:System.Runtime.CompilerServices.IUnion>-->. You can check for any union value at runtime by using `IUnion`:

Expand All @@ -211,9 +222,6 @@ When you declare a `union` type, the compiler generates a struct that implements
}
```

> [!IMPORTANT]
> In .NET 11 Preview 2, these types aren't included in the runtime. To use union types, you must declare them in your project. They'll be included in a future .NET preview.

## C# language specification

For more information, see the [Unions](~/_csharplang/proposals/unions.md) feature specification.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ To correct these errors, apply the following changes to your union members:

- **CS9372**: *An expression of type cannot be handled by this pattern, see additional errors at this location.*

This error arises when you use an incorrect pattern form with a union type. For the complete rules on union pattern matching, see [union matching](../builtin-types/union.md#union-matching).
This error arises when you use an incorrect pattern form with a union type. For the complete rules on union pattern matching, see [union matching](../builtin-types/union.md#union-pattern-matching).

To correct this error, use the correct pattern form when matching against a union value (**CS9372**). Patterns on a union apply to the union's `Value` property, not the union value itself. If the compiler reports that a pattern can't handle the expression, check that you're matching against the case types listed in the union declaration. Review the additional errors at the same location for details about which pattern is invalid.
8 changes: 4 additions & 4 deletions docs/csharp/language-reference/operators/patterns.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Patterns - Pattern matching using the is and switch expressions."
description: "Learn about the patterns supported by the `is` and `switch` expressions. Combine multiple patterns using the `and`, `or`, and `not` operators."
ms.date: 03/20/2026
ms.date: 06/05/2026
f1_keywords:
- "and_CSharpKeyword"
- "or_CSharpKeyword"
Expand Down Expand Up @@ -302,7 +302,7 @@ For more information, see [List pattern](~/_csharpstandard/standard/patterns.md#

## Union patterns

Starting with C# 15, when the incoming value of a pattern is a [union type](../builtin-types/union.md), patterns automatically *unwrap* the union. They apply to the union's `Value` property rather than the union value itself. This behavior makes the union transparent to pattern matching:
Starting with C# 15, when the incoming value of a pattern is a [union type](../builtin-types/union.md), patterns generally *unwrap* the union. The pattern applies to the union's `Value` property rather than the union value itself. This behavior makes the union transparent to pattern matching:

```csharp
public record class Cat(string Name);
Expand All @@ -316,13 +316,13 @@ string Describe(Pet pet) => pet switch
};
```

Two patterns are exceptions: the `var` pattern and the discard `_` pattern apply to the union value itself, not its `Value` property.
Three patterns are exceptions: the discard `_` pattern, the `var` pattern, and the `not` pattern apply to the union value itself, not its `Value` property.

The `null` pattern checks whether the union's `Value` is null. For class-based unions, `null` also succeeds when the union reference itself is null.

When a union type provides the *non-boxing access pattern* (`HasValue` and `TryGetValue` members), the compiler uses those members to avoid boxing value-type cases during pattern matching.

For more information, see [Union matching](../builtin-types/union.md#union-matching). For the specification, see [Unions](~/_csharplang/proposals/unions.md).
For more information, see [Union matching](../builtin-types/union.md#union-pattern-matching). For the specification, see [Unions](~/_csharplang/proposals/unions.md).

## C# language specification

Expand Down
Loading
Loading