Dev Encyclopedia
ArticlesTools

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

© 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /C# Union Types: Migrating from OneOf in .NET 11 (2026 Guide)
dotnet8 min read

C# Union Types: Migrating from OneOf in .NET 11 (2026 Guide)

C# 15 native union types just shipped in .NET 11 preview. Here is how they compare to the OneOf library, what migration actually looks like, and the boxing tradeoff to know about.

By Dev EncyclopediaPublished June 23, 2026
On this page

On this page

  • What Shipped, and When
  • The Problem OneOf Has Been Solving
  • OneOf vs Native Union: Side by Side
  • Migrating a Real OneOf Method
  • The Exhaustiveness Difference
  • The Boxing Tradeoff
  • How to Try It Today
  • Should You Migrate Now?
  • Frequently Asked Questions

C# is getting native union types. After years of community requests, third-party workarounds, and a design process publicly tracked since 2024, the union keyword shipped in .NET 11 Preview 2 this April, with full runtime support landing by Preview 5. GA is expected with C# 15 in November 2026.

If you have used the OneOf NuGet package (and a huge number of C# codebases have, because it has been the de facto answer to this problem for years), this changes things. Here is exactly what changes, and what migrating actually looks like.

What Shipped, and When

The union keyword first appeared in .NET 11 Preview 2 (April 2026). Runtime types (UnionAttribute and IUnion) landed in Preview 5. The Microsoft Learn documentation was last updated June 11, 2026, and Microsoft's Build 2026 recap calls union types one of the headline .NET 11 features.

GA is expected with C# 15 / .NET 11 in November 2026. The syntax has already shifted between preview releases, so treat everything below as current-preview behavior that may still evolve before the final release.

The Problem OneOf Has Been Solving

Before any of this, if a method needed to return one of several possible types, C# developers had genuinely bad options. Throwing an exception for an expected outcome like "customer not found" is semantically wrong, because it is not exceptional, it is a normal result. Returning a nullable tuple plus a separate error string means callers have to remember which field to check, and the compiler gives no help if they forget.

Building a custom result class with a discriminator enum is the closest to correct, but it is boilerplate you write and maintain by hand, and the compiler still cannot tell you if you missed a case in a switch statement.

OneOf became the standard library answer: OneOf<T0, T1, T2> as a generic wrapper type, with .Match() and .Switch() methods that require a handler for every generic parameter.

OneOf vs Native Union: Side by Side

Here is the same method signature, three ways.

Hand-rolled (the pre-OneOf approach):

csharp
public abstract class OrderResult { }
public class Receipt : OrderResult { public int ReceiptId; }
public class InsufficientFunds : OrderResult { }
public class ProductNotFound : OrderResult { }

public OrderResult PlaceOrder(int productId, int payment) { ... }

With OneOf:

csharp
public OneOf<Receipt, InsufficientFunds, ProductNotFound>
    PlaceOrder(int productId, int payment) { ... }

// Calling code
result.Switch(
    receipt => Console.WriteLine($"Order placed: {receipt.ReceiptId}"),
    funds => Console.WriteLine("Insufficient funds"),
    notFound => Console.WriteLine("Product not found")
);

With a native union:

csharp
public union OrderResult(Receipt, InsufficientFunds, ProductNotFound);

public OrderResult PlaceOrder(int productId, int payment) { ... }

// Calling code
var message = result switch
{
    Receipt r => $"Order placed: {r.ReceiptId}",
    InsufficientFunds => "Insufficient funds",
    ProductNotFound => "Product not found",
};

The native version reads like ordinary C# pattern matching, because it is. No .Switch() API to learn, no generic type parameter list to keep in sync with your handler lambdas.

OneOf LibraryNative Union (C# 15)
Declaration syntaxOneOf<T0, T1, T2> (generic params)union Name(T0, T1, T2)
Case handling.Match() / .Switch() lambdasswitch expression (standard C#)
ExhaustivenessAPI-level (.Match requires all lambdas)Compiler-level (switch must cover all cases)
IDE supportBasic (NuGet package)Full (language-level, IntelliSense, analyzers)
Requires NuGet packageYesNo (built into the language)
Min framework.NET Standard 2.0+.NET 11+ (preview)

Migrating a Real OneOf Method

Take an existing method:

csharp
public OneOf<Success<User>, NotFound, ValidationError>
    GetUser(int id) { ... }

Convert the type declaration first:

csharp
public union UserResult(Success<User>, NotFound, ValidationError);
public UserResult GetUser(int id) { ... }

Then convert every call site. A .Match() call becomes a switch expression:

csharp
// Before (OneOf)
var response = result.Match(
    success => Ok(success.Value),
    notFound => NotFound(),
    error => BadRequest(error.Message)
);

// After (native union)
var response = result switch
{
    Success<User> s => Ok(s.Value),
    NotFound => NotFound(),
    ValidationError e => BadRequest(e.Message),
};

The conversion is mostly mechanical for .Match() calls returning a value. .Switch() calls (void, side-effecting) convert the same way but without the assignment.

💡 Automate the mechanical part

Convert your own OneOf code instantly with OneOfToUnion. Paste your OneOf type declarations and .Match()/.Switch() calls, get back the native union equivalent, ready to review and paste.

The Exhaustiveness Difference

OneOf enforces exhaustiveness through its API shape: .Match() literally requires a lambda parameter for every generic type argument, so you cannot compile if you forget one. But .Switch() calls written by hand, or any code that manually inspects .IsT0 / .IsT1 properties with an else fallback, can silently swallow a new case if someone later adds a fourth generic parameter to the OneOf<> declaration and forgets to update every consumer.

Native unions enforce exhaustiveness at the compiler level for switch expressions, full stop. Add a fourth case type to your union declaration, and every switch expression handling that union anywhere in your codebase fails to compile until you add a case for it. This is a meaningfully stronger guarantee than OneOf's runtime-API-shaped enforcement.

ℹ Compiler vs API enforcement

With OneOf, adding a new case type to the generic parameters silently breaks any .Switch() or .IsT* call site that uses a default fallback. With native unions, the compiler flags every incomplete switch expression as an error. You find breakage at build time, not at runtime.

The Boxing Tradeoff

This is the detail that matters most for performance-sensitive migrations. Under the hood, a union is a generated struct holding a single object? property. If every case type in your union is a reference type (a class), storing a value just stores that one reference with no boxing.

But if you mix value types (structs) with reference types in the same union, say union Result(int, ErrorInfo) where ErrorInfo is a class, the int case gets boxed to fit into that object? slot. This is functionally correct but has the usual boxing cost: a heap allocation and GC pressure you would not have with a plain int return.

⚠ Boxing check

If your union is all reference types, there is no boxing concern. If you are mixing structs and classes in a hot path processing millions of operations per second, benchmark before migrating. Consider a manually-implemented non-boxing shape using HasValue/TryGetValue instead of the compiler-generated default.

How to Try It Today

Install the .NET 11 Preview SDK, then configure your project file:

xml — YourProject.csproj
<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

As of Preview 5, the UnionAttribute and IUnion interface ship in the runtime, so you do not need to hand-declare them as you did in earlier previews. IDE support is available in current Visual Studio Insiders builds and the latest C# DevKit Insiders build.

If you are running CI with GitHub Actions, add the preview SDK to your setup-dotnet step and ensure your test commands pass the preview flag. Similar to how teams approached the TypeScript 7 migration, experiment in a branch before committing production code.

Should You Migrate Now?

If you are starting a new project or an experimental branch, native unions are genuinely worth using today. They are more concise than OneOf, and the compiler-enforced exhaustiveness is a real improvement. Report any rough edges to the C# team; previews exist specifically to gather this kind of feedback before the syntax locks in at GA.

For existing production code currently using OneOf: there is no urgency. OneOf works correctly today, has years of battle-testing, and the preview syntax has already shifted between releases this year. Wait for the C# 15 / .NET 11 GA release in November 2026 before planning a real migration. By then the final syntax and runtime behavior will be locked, and you will do the conversion once instead of chasing a moving target.

Frequently Asked Questions

Frequently Asked Questions

Is C# union the same as OneOf?

They solve the same problem: representing "this value is exactly one of these types." The key differences are where they live and how they enforce correctness. OneOf is a third-party NuGet package that uses generic type parameters and runtime API patterns (.Match(), .Switch()). Native unions are a language feature with a dedicated union keyword, compiler-enforced exhaustiveness in switch expressions, and full IDE integration.

For most use cases, a native union is a direct replacement for an equivalent OneOf declaration. The main migration consideration is the boxing tradeoff for unions mixing value and reference types.

Do C# unions cause boxing?

Only when value types (structs) and reference types (classes) are mixed in the same union. A union of all reference types stores one reference with no boxing. A union of all value types also avoids boxing when the compiler can use a specialized layout. Mixing the two causes value type cases to be boxed into the internal object? storage.

For most business logic (where case types are classes like Success, NotFound, ValidationError), boxing is not a concern.

When will C# union types be stable?

GA is expected with C# 15 / .NET 11 in November 2026. The union keyword first appeared in .NET 11 Preview 2 (April 2026), and runtime support landed in Preview 5. The syntax has evolved between previews, so the final GA syntax may differ slightly from current preview behavior.

Can I use union types in .NET 10?

No. Native union types require .NET 11 (currently in preview). .NET 10 does not include the union keyword or the UnionAttribute / IUnion runtime types. If you are on .NET 10 or earlier and need this pattern, continue using the OneOf NuGet package.

What is the difference between a union and a discriminated union in C#?

In C# 15, the terms are effectively synonymous. The union keyword declares what functional programming languages call a "discriminated union" or "tagged union": a type that is exactly one of a closed set of case types, with a compiler-tracked discriminator so pattern matching can determine which case a value holds.

The "discriminated" qualifier distinguishes these from C/C++ unions, which share memory without tracking which member is active. C# unions always know which case is active, making them safe to pattern match against.

Is there a tool to convert OneOf code to native unions?

Yes. OneOfToUnion converts OneOf<T0, T1, ...> type declarations and .Match() / .Switch() call sites to the equivalent native union declaration and switch expression code. It runs entirely in your browser and produces a starting-point conversion that you should always review and compile before committing.

Related Articles

typescript

TypeScript 7 Migration Guide: tsgo, Breaking Changes, Build Times

Migrate to TypeScript 7 (tsgo): install the beta, fix the 4 breaking changes, update tsconfig, and decide if upgrading now is worth it.

Jun 15, 2026·9 min read
security

GitHub Actions Security: 7 Misconfigurations to Avoid

The 7 GitHub Actions misconfigurations behind real supply chain attacks: weak GITHUB_TOKEN scope, pull_request_target, unpinned actions, script injection.

Jun 12, 2026·14 min read

On this page

  • What Shipped, and When
  • The Problem OneOf Has Been Solving
  • OneOf vs Native Union: Side by Side
  • Migrating a Real OneOf Method
  • The Exhaustiveness Difference
  • The Boxing Tradeoff
  • How to Try It Today
  • Should You Migrate Now?
  • Frequently Asked Questions
Advertisement