C# 15 introduce le union: basta OneOf e Results per modellare i tipi (.NET 11 Preview 6)
Da anni gli sviluppatori C# aspettano un modo nativo per dire “questo valore è esattamente uno tra questi tipi, e nient’altro”, con il compilatore in grado di garantirlo. In assenza di un supporto di linguaggio, la community si è arrangiata con pattern come Results<T> di ASP.NET Core Minimal API o con librerie come OneOf. Funzionano, ma con un limite strutturale: se aggiungi un quarto caso possibile a una funzione che prima ne restituiva tre, nessuno ti avvisa che uno switch a valle non gestisce il nuovo tipo. Manca l’esaustività, che è poi il punto centrale delle discriminated union.
Con .NET 11 Preview 6 (rilasciata il 14 luglio 2026) e C# 15, questa lacuna si chiude: arriva la parola chiave union, insieme a un intero pacchetto di funzionalità correlate (conversioni implicite, pattern matching esaustivo, gestione della nullabilità) che rendono i union type un cittadino di prima classe del linguaggio.
La sintassi: dichiarare un union type
La forma più semplice è la union declaration, una sintassi compatta che genera automaticamente uno struct:
public union Pet(Cat, Dog, Bird);
Questa singola riga dice al compilatore che un Pet è esattamente un Cat, un Dog o un Bird, e nient’altro. Sotto il cofano, il compilatore genera uno struct che implementa l’interfaccia System.Runtime.CompilerServices.IUnion e conserva il valore effettivo in una proprietà Value di tipo object?.I union generici sono immediatamente utili per modellare risultati di operazioni che possono fallire, un caso d’uso ricorrentissimo in codice di produzione:
public union Result<TSuccess, TError>(TSuccess, TError);
Result<User, Error> Register(string username)
{
if (string.IsNullOrEmpty(username))
return new Error("Username cannot be empty");
return new User(username);
}
Da notare l’ultima riga: nessun wrapping esplicito, nessuna chiamata a un factory method. Il valore User o Error viene convertito implicitamente in Result<User, Error> grazie alle union conversion generate automaticamente per ogni case type.
Pattern matching esaustivo, finalmente
Il vero salto di qualità è nel matching. Se dimentichi un caso in uno switch su un union type, il compilatore te lo dice:
string Describe(Pet pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
// CS8509: switch expression does not handle all
// possible values of its input type (Bird)
};
Aggiungendo il caso mancante, l’avviso scompare, e non serve più il classico _ => throw new InvalidOperationException("unreachable") per zittire il compilatore:
string Describe(Pet pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Il punto interessante è che questa esaustività è dinamica: se in futuro aggiungi un nuovo case type al union (per esempio un quarto animale), ogni switch esistente che non lo gestisce si illumina di un warning. Per chi ha inseguito bug di produzione causati da un case dimenticato in un evento di dominio, è un cambiamento non da poco.I pattern si applicano direttamente al union, senza bisogno di accedere manualmente a .Value: il compilatore effettua l’unwrapping in automatico, anche negli if pattern classici:
if (pet is Dog d)
{
Console.WriteLine(d.Name);
}
Gestione del null
Il valore di default di un union type ha Value == null, ed è possibile intercettarlo esplicitamente con il pattern null:
string Describe(Pet pet) => pet switch
{
null => "nessun animale",
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
La necessità o meno del ramo null dipende dal fatto che il parametro o il campo union-typed sia nullable: l’analisi di nullabilità del compilatore guida correttamente questi casi, in coerenza con le annotazioni #nullable enable già in uso nella maggior parte delle codebase moderne.
Un caso pratico: modellare eventi di dominio
I union type diventano davvero interessanti quando li si combina con i record per il domain modeling, per esempio in un sistema di elaborazione ordini con eventi distinti:
public record OrderPlaced(Guid OrderId, decimal Total);
public record OrderShipped(Guid OrderId, string TrackingNumber);
public record OrderCancelled(Guid OrderId, string Reason);
public union OrderEvent(OrderPlaced, OrderShipped, OrderCancelled);
string Summarize(OrderEvent evt) => evt switch
{
OrderPlaced p => $"Ordine {p.OrderId} piazzato per {p.Total:C}",
OrderShipped s => $"Ordine {s.OrderId} spedito, tracking: {s.TrackingNumber}",
OrderCancelled c => $"Ordine {c.OrderId} annullato: {c.Reason}",
};
Quando tra tre mesi qualcuno aggiungerà OrderRefunded al union, ogni handler che non lo gestisce lancerà un warning in fase di compilazione, invece di fallire silenziosamente in produzione. È il compilatore che fa da revisore di esaustività al posto tuo.I union possono anche esporre metodi e proprietà calcolate (ma non campi d’istanza), utile per incapsulare logica di conversione:
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException(),
};
}
Union personalizzati con l’attributo [Union]
La parola chiave union genera sempre uno struct. Se per motivi di identità, ereditarietà o layout di memoria serve un tipo reference, è possibile costruire un union personalizzato applicando direttamente l’attributo System.Runtime.CompilerServices.UnionAttribute e implementando l’interfaccia IUnion:
[System.Runtime.CompilerServices.Union]
public class Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
Nella grande maggioranza dei casi, però, la sintassi union compatta copre già ogni esigenza pratica: l’approccio con attributo è pensato per scenari con requisiti specifici che lo struct generato non può soddisfare.
Come provarlo oggi
I union type sono attualmente in preview. Per sperimentarli serve l’SDK .NET 11 Preview e questa configurazione nel file di progetto:
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
Vale la pena ricordare che si tratta di una feature specification ancora in evoluzione: alcuni dettagli di comportamento (per esempio le regole esatte su nullabilità di default o sulle conversioni sollevate/”lifted”) sono ancora oggetto di discussione nel repository dotnet/csharplang, quindi è normale osservare piccoli cambiamenti tra una preview e l’altra prima del rilascio finale.
Conclusioni
F# ha le discriminated union fin dalle origini, e la richiesta di un equivalente in C# (csharplang issue #113) è aperta da anni. Con l’arrivo dei union type, non ogni problema di modellazione richiede più una gerarchia di classi: per rappresentare “uno tra un insieme chiuso di tipi”, ora esiste un costrutto di linguaggio dedicato, con conversioni implicite, pattern matching esaustivo e un’ottima integrazione con i record esistenti. Per chi lavora ogni giorno con API che possono restituire risultati eterogenei, o con event sourcing e domain modeling, è una delle novità più concrete della prossima versione di C#.
Fonte: .NET Blog, C# language reference proposal: Unions e Maarten Balliauw.
Back in 2023, I wrote about discriminated unions in C# and how C# developers had to work around the lack of language support using things like ASP.NET Core’s Results type or the OneOf NuGet package.
Maarten Balliauw (Maarten Balliauw {blog})
Marcel Geveler
in reply to netzpolitik.org • • •Guido Reichert
in reply to netzpolitik.org • • •2k115
in reply to netzpolitik.org • • •Lasse Gismo (Friendica Antifa)
in reply to netzpolitik.org • • •@netzpolitik.org
Warum nur hab' ich CÖ gewählt...
Wang Seppel de Opel
in reply to netzpolitik.org • • •Brainworms, wir sind Viele!
in reply to netzpolitik.org • • •Grün fickt Volk. Özdemir WILL Totalüberwachung mit der cdafdu.
Grün ist mittlerweile kackbraun. 🤮🤮🤮🤮
Der Trollkin (Erpse)
in reply to netzpolitik.org • • •Hat wirklich jemand geglaubt, der giftgrüne Parteibonze würde auf die überwältigende Mehrheit seiner Parteimitglieder (92%) hören?
Da der doch glatt so liberal und macht was er will ... und ihn an der Macht hält!
So schnell geht es nunmal:
Gestern noch als "Bürderrechtspartei" mit Mitgliederbeteiligung gefeiert und heute die Machtergreifung zu spüren bekommen!
"Wollt ihr die totale Überwachung? nee, egal ich drücke sie euch trotzdem auf!"
vampirdaddy
in reply to netzpolitik.org • • •Formukierungsfehler?
Müsste das nicht statt
"Dabei verlangen die Mitglieder _alles andere_ als eine grundlegende Wende."
nicht heißen
"Dabei verlangen die Mitglieder _nichts anderes_ als eine grundlegende Wende."
WarpinWolf
in reply to netzpolitik.org • • •@AwetTesfaiesus
soviel zum Thema Basisdemokratie, oder "links" grün versifft... Mach mir den Schily...
Ich sag ja, der beste sozialdemokratische Kanzler der letzten 30 Jahre hieß Angela, und der beste CDU Landesfürst Kretschmann. Und Cem will besser sein...
Bauarbeiter* for Future
in reply to netzpolitik.org • • •KielKontrovers Blog
in reply to netzpolitik.org • • •Karl Dezeti ₀ ∫¹³⁷ ♀☺⸎ⁱ☻♂ ∂ℹ
in reply to netzpolitik.org • • •Peter König - klar: Antifa 🧩
in reply to netzpolitik.org • • •Meine Verständnis von Grüner Realpolitik:
Unter dem Schlagwort "Anschlussfähigkeit" hat sich eine autoritär-neoliberale Legacy aus den 2000er und 2010er Jahren in die Grünen eingeschlichen:
Hinter vorgeblicher Loyalität gegenüber grünen Oberzielen ist die relevante grüne Realpolitik von blindem Gehorsam gegenüber transatlantischer Bevormundung geprägt - die eigentliche Grüne Ziele im Zweifel verrät.
BW und diese Diskussion stehen dafür genauso wie unsere neue "Kriegsliebe".
Paul
in reply to netzpolitik.org • • •Sui
in reply to netzpolitik.org • • •Schlecht, wel framende Überschrift. Besser "Grüne Basis stimmt gegen Palantir, aber nicht gegen anlasslose Massenüberwachung"
An den Kommentaren hier sieht man, dass die Propagandastrategie der Grünen Basis aufgeht: viele denken fälschlicherweise, dass die sie gegen die Massenüberwachung gestimmt hätten.
Dabei haben sie nur darüber abgestimmt, welche Software benutzt werden soll. Die Überwachung selbst wird auch von der Basis befürwortet.
Steht auch alles im Artikel.