Hinweis: Dieser Podcast und dieses Video wurden mithilfe von KI erstellt. Dabei wurden die Originalinhalte und technischen Erkenntnisse des Autors des Blogbeitrags adaptiert.
Eine Discriminated Union (oft auch Tagged Union, Algebraic Data Type oder Sum Type genannt) ist ein Datentyp, der genau einen von mehreren möglichen Typen enthalten kann, wobei jederzeit klar ist, welcher Typ gerade enthalten ist. Viele moderne Sprachen (z. B. F#, Rust, Swift, TypeScript) beherrschen dieses Konzept bereits oder erlauben die Nachbildung eines solchen Konzepts (z. B. Kotlin). In C# musste man sich bisher mit dem Basistyp System.Object, Vererbung, Interfaces, Pattern Matching oder Lösungen auf Basis generischer Typen wie der Bibliothek OneOf [1] behelfen.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Discriminated Unions in Form von Union Types
Echte Discriminated Unions werden von C#-Entwicklerinnen und Entwicklern seit langem ersehnt und sind nun in C# 15.0 in Form sogenannter „Union Types“ enthalten. C# 15.0 ist ein Teil von .NET 11.0, das sich zum Redaktionsschluss für diesen Beitrag im Stadium Preview 3 befindet und im November 2026 erscheinen soll. Der Visual-Studio-Editor funktioniert für Union Types in der Visual-Studio-Version 2026 Insiders 11709.129 zusammen mit .NET 11.0 Preview 3.
Einen Union Type in C# 15.0 erstellen Entwicklerinnen und Entwickler mit dem neuen C#-Schlüsselwort union unter Angabe eines Namens und eines oder mehrerer Typen, z. B.
public union UnionName(Typ1, Typ2, Typ3);
Dabei müssen die in der Union angegebenen .NET-Typen keinerlei Gemeinsamkeiten besitzen: Weder eine Basisklasse noch eine Schnittstelle müssen sie teilen. Das Schlüsselwort null darf man nicht als Typ angeben. Nullable Value Types (z. B. int?) und Nullable Reference Types (Person?) sind aber erlaubt.
Einer Variablen des Typs UnionName kann man dann sowohl Instanzen von Typ1 und Typ2 als auch Typ3 sowie ggf. null zuweisen. Jede andere Objekttypzuweisung wird aber vom Compiler verhindert.
Mit dem Operator is oder Pattern Matching können Entwicklerinnen und Entwickler abfragen, welchen konkreten Typ die Union-Typvariable enthält. Während beim Pattern Matching automatisch ein Casting auf den Zieltyp erfolgt, muss man ohne Pattern Matching die Eigenschaft Value nutzen, um an den konkreten Typ zu kommen. Allerdings liefert Value den Typ System.Object, sodass wieder ein Casting erforderlich wird.
Beim Einsatz von switch-Ausdrücken warnt der Compiler, wenn nicht alle Fälle abgefragt werden, mit der Meldung CS8509: „The switch expression does not handle all possible values of its input type (it is not exhaustive).“
Voraussetzungen
Allerdings fehlt auch in .NET 11.0 Preview 3 immer noch ein Stück Code in der Basisklassenbibliothek für die Union Types. Neben dem Property
<LangVersion>preview</LangVersion>
in der Projektdatei müssen Entwicklerinnen und Entwickler daher auch den Inhalt von Listing 1 mit der Implementierung der Annotation [Union] via Klasse UnionAttribute in jedes einzelne C#-Projekt aufnehmen, das Union Types nutzen will.
Listing 1: Die Klasse UnionAttribute fehlt derzeit noch in der .NET-Basisklassenbibliothek
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Hintergrund ist, dass der C#-15.0-Compiler alle Union Types automatisch mit der Annotation [Union] versieht, was man erkennt, wenn man den Union Type
public union PersonOperationResult(Person?, string, int?, Exception);
mit ILSpy [2] dekompiliert (Listing 2).
Listing 2: Dekompilat eines Union Types
// NET11_Console, Version=11.3.0.0, Culture=neutral, PublicKeyToken=null
// NET11_Console.CS15.PersonOperationResult
using System;
using System.Runtime.CompilerServices;
using NET11_Console.CS15;
[Union]
public struct PersonOperationResult : IUnion
{
public object? Value { get; }
[CompilerGenerated]
public PersonOperationResult(Person? value)
{
Value = value;
}
[CompilerGenerated]
public PersonOperationResult(string value)
{
Value = value;
}
[CompilerGenerated]
public PersonOperationResult(int value)
{
Value = value;
}
[CompilerGenerated]
public PersonOperationResult(Exception value)
{
Value = value;
}
}
Ein Praxisbeispiel: verschiedene Rückgabetypen einer Methode
Listing 3 zeigt einen C# 15.0 Union Type an einem typischen Einsatzbeispiel für Discriminated Unions: Eine Operation der Geschäftslogik besitzt verschiedenartige Rückgabetypen: Sie kann neben einem konkreten Ergebnisobjekt auch mit Zeichenketten, Zahlen oder Exception-Objekten verschiedene Fehlerfälle signalisieren (Abb. 1).
Listing 3: Union Type für differenzierte Rückgabetypen einer Geschäftslogikmethode
#nullable enable
namespace NET11_Console.CS15;
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public string Website { get; set; }
public override string ToString()
{
return $"Person: #{ID} Name: {Name} Website: {Website}";
}
}
public union PersonOperationResult(Person?, string, int?, Exception);
class BL
{
public PersonOperationResult GetPerson(int ID)
{
try
{
if (ID <= 0) return "Ungültige Person-ID";
if (ID == 123) return new Person() { ID = ID, Name = "Dr. Holger Schwichtenberg", Website = "www.IT-Visions.de" };
if (ID == 0815) throw new ApplicationException("Anwendungsfehler");
return ID; // Person nicht gefunden
}
catch (Exception ex)
{
return ex;
}
}
}
class UnionTypeDemo
{
public void Run()
{
CUI.Demo(nameof(UnionTypeDemo));
BL BL = new();
CUI.H1("\nLade Person mit ID #123");
PersonOperationResult result1 = BL.GetPerson(123);
PrintResult(result1);
CUI.H1("\nLade Person mit ID #0 (Fehlerfall)");
PersonOperationResult result3 = BL.GetPerson(-1);
PrintResult_PatternMatching(result3);
CUI.H1("\nLade Person mit ID #101 (nicht gefunden)");
PersonOperationResult result4 = BL.GetPerson(101);
PrintResult(result4);
CUI.H1("\nLade Person mit ID #0815 (Anwendungsfehler)");
PersonOperationResult result5 = BL.GetPerson(0815);
PrintResult(result5);
CUI.H1("\nTest mit Null-Wert");
PersonOperationResult result2 = null;
PrintResult_PatternMatching(result2);
// Nicht erlaubt
//PersonOperationResult result = new FileInfo(@"c:\temp\Datei.xy");
}
private static void PrintResult(PersonOperationResult result)
{
if (result is int)
{
CUI.Error($"Person #{result.Value} nicht gefunden");
}
else if (result is string)
{
CUI.Error($"Fehler: {result.Value}");
}
else if (result is Exception)
{
CUI.Error($"Fehler: {(result.Value as Exception).Message}");
}
else if (result is Person)
{
CUI.Print(result.Value);
}
else if (result is null)
{
CUI.Warning("Null");
}
}
private static void PrintResult_PatternMatching(PersonOperationResult result)
{
if (result is int i)
{
CUI.Error($"Person #{i} nicht gefunden");
}
else if (result is string s)
{
CUI.Error($"Fehler: {s}");
}
else if (result is Exception ex)
{
CUI.Error($"Fehler: {ex.Message}");
}
else if (result is Person p)
{
CUI.Print(p);
}
else if (result is null)
{
CUI.Warning("Null");
}
}
private static void PrintResult_Switch(PersonOperationResult result)
{
Console.WriteLine(result switch
{
Person p => $"Person: {p.Name}",
string s => $"Fehler: {s}",
Exception ex => $"Fehler: {ex.Message}",
int i => $"Person #{i} nicht gefunden",
null => "Null-Wert"
});
}
}

Abb. 1: Das Beispiel demonstriert verschiedene Fälle beim Laden einer „Person“ anhand einer ID
Ausblick
Union Types haben das Potenzial, in einigen .NET-Anwendungsframeworks für Syntaxvereinfachungen zu sorgen, siehe Roadmap für ASP.NET WebAPIs und ASP.NET Core SignalR sowie Blazor [3]. So könnte man Union Types zum Beispiel bei Typed Results in WebAPI-Operationen einsetzen.
Weitere Features in C# 15.0: Parameter für Collection Expressions in C# 15.0
Seit C# 12.0 gibt es in C# die Collection Expressions zur vereinfachten Initialisierung von Objektmengen, z. B.
List<int> magicNumbers1 = [6, 7, 42];
Diese Syntax mit den eckigen Klammern war aber nicht möglich, wenn man bei der Initialisierung der Menge Konstruktorparameter setzen wollte, z. B. die Kapazität einer Objektmenge:
List<int> magicNumbers2 = new(100) { 6, 7, 42 };
Beziehungsweise sprechender:
List<int> magicNumbers3 = new(capacity:100) { 6, 7, 42 };
Diesen Sonderfall kann man in C# 15.0 mit Collection Expression Arguments nun wie folgt ausdrücken, wobei das die Syntax aber nicht wesentlich kürzer macht:
List<int> magicNumbers3 = [with(capacity: 100), 6, 7, 42];
Asynchronität in der .NET Runtime
Asynchrone Programmierung mit async und await gibt es seit Jahren in .NET. Anstelle der reinen Compilerlösung auf Basis von State Machines liefert Microsoft nun eine neue Laufzeitumgebung, welche die asynchrone Ausführung eigenständig unterstützt. Mit der neuen Runtime werden Stacktraces übersichtlicher.
Allerdings erfordert die neue Unterstützung für Asynchronität in der Laufzeitumgebung aktuell noch, dass diese Neuerungen in der Projektdatei separat aktiviert werden:
<PropertyGroup>
<Features>runtime-async=on</Features>
</PropertyGroup>
Weitere Pläne für C# 15.0
Was sonst noch für C# 15.0 geplant ist, sieht man im „Working Set“ des C# Language Status auf GitHub [4]:
-
Klassen können mit dem Schlüsselwort closed versehen werden. Das verhindert, dass in einer anderen Assembly eine Vererbung von der Klasse möglich ist [5].
-
Collection Expressions sollen auch für Dictionaries nutzbar sein [6], z. B. SortedDictionary<string, int> dic = [“six”:6, “seven”:7, “forty two”:42];
-
Die in C# 14.0 eingeführten Extensions sollen auch Indexer implementieren können [7].
Links & Literatur
[1] https://github.com/mcintyre321/OneOf
[2] https://github.com/icsharpcode/ILSpy
[3] https://github.com/dotnet/aspnetcore/issues/64599
[4] https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md
[5] https://github.com/dotnet/csharplang/issues/9499
Author
🔍 Frequently Asked Questions (FAQ)
1. Was sind Union Types in C# 15.0?
Union Types sind die Umsetzung von Discriminated Unions in C# 15.0. Ein solcher Datentyp kann genau einen von mehreren definierten Typen enthalten, ohne dass diese eine gemeinsame Basisklasse oder Schnittstelle benötigen. Dadurch lassen sich beispielsweise unterschiedliche Ergebnis- und Fehlertypen über einen gemeinsamen Rückgabetyp abbilden.
2. Wie definiert man einen Union Type in C# 15.0?
Ein Union Type wird mit dem neuen Schlüsselwort union sowie einem Namen und den zulässigen Typen definiert, zum Beispiel public union UnionName(Typ1, Typ2, Typ3);. Die angegebenen .NET-Typen müssen weder eine gemeinsame Basisklasse noch eine gemeinsame Schnittstelle besitzen.
3. Können Union Types in C# 15.0 null enthalten?
Das Schlüsselwort null kann nicht direkt als Typ innerhalb einer Union angegeben werden. Nullable Value Types wie int? und Nullable Reference Types wie Person? sind jedoch erlaubt. Einer Union-Typvariable kann abhängig von den enthaltenen Typen auch null zugewiesen werden.
4. Wie ermittelt man den enthaltenen Typ eines Union Types?
Der konkrete Typ eines Union-Werts kann mit dem is-Operator oder mit Pattern Matching geprüft werden. Beim Pattern Matching erfolgt gleichzeitig die Typkonvertierung auf den passenden Zieltyp. Ohne Pattern Matching kann über die Eigenschaft Value auf den Wert zugegriffen werden, die allerdings System.Object zurückliefert und daher ein Casting erfordert.
5. Prüft der C#-Compiler Union Types bei switch-Ausdrücken auf Vollständigkeit?
Ja. Werden bei einem switch-Ausdruck nicht alle möglichen Fälle eines Union Types berücksichtigt, warnt der Compiler mit CS8509, dass der Ausdruck nicht alle möglichen Werte des Eingabetyps behandelt. Dadurch können fehlende Fälle bereits beim Kompilieren sichtbar werden.
6. Wofür eignen sich Union Types in der Praxis?
Union Types eignen sich unter anderem für Methoden, die unterschiedliche Ergebnis- und Fehlertypen zurückgeben müssen. Das Beispiel im Artikel verwendet einen PersonOperationResult, der eine Person, einen String, eine Zahl oder eine Exception enthalten kann. Als weiterer möglicher Einsatzbereich werden Typed Results in ASP.NET-WebAPI-Operationen genannt.





