{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"},{"name":"Jendrik Brack","url":"https://daily-devops.net/authors/jendrik/"}],"description":"Recent content in C# 15 Language Features in Practice on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/csharp15/feed.json","home_page_url":"https://daily-devops.net/tags/csharp15/","icon":"https://daily-devops.net/images/logo_hu_5926de77762241ba.png","items":[{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIf you have ever hand-rolled a \u003ccode\u003eResult\u003c/code\u003e type as a sealed abstract class with two private-constructor subclasses, or reached for the \u003ca href=\"https://github.com/mcintyre321/OneOf\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eOneOf\u003c/a\u003e NuGet package because the language gave you nothing better, you already know the problem C# 15\u0026rsquo;s union types are aimed at. That gap has existed since a 2017 csharplang proposal first floated the idea, and it is finally something you can install and try.\u003c/p\u003e\n\u003cp\u003eThis is the first of a short series looking at C# 15 candidate and preview language features: what they are, where the design actually stands, and whether they earn a spot in a production codebase or just a spot in a conference talk.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-shipped-and-where\"\u003e\u003ca href=\"/posts/csharp-15-union-types-expectations/#what-shipped-and-where\" title=\"What Shipped, and Where\"\u003eWhat Shipped, and Where\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTwo related but separately shippable features landed together in .NET 11 preview SDKs, with official documentation on Microsoft Learn covering \u003ca href=\"https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/union\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eunion types\u003c/a\u003e and the \u003ca href=\"https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-15.0/unions\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eunions proposal\u003c/a\u003e: nominal \u003ccode\u003eunion\u003c/code\u003e type declarations, and a \u003ccode\u003eclosed\u003c/code\u003e modifier for exhaustive class hierarchies. The runtime plumbing for unions (an \u003ccode\u003eIUnion\u003c/code\u003e interface and supporting attributes) arrived in .NET 11 Preview 5, and you need \u003ccode\u003eLangVersion=preview\u003c/code\u003e to compile any of it. Design work continues on the working group\u0026rsquo;s \u003ca href=\"https://github.com/dotnet/csharplang/issues/8928\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003echampion issue #8928\u003c/a\u003e, driven by a dedicated working group since roughly 2022, with Fred Silberberg as the named champion alongside Mads Torgersen and Matt Warren.\u003c/p\u003e\n\u003cp\u003eHere is the syntax as documented today:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eunion\u003c/span\u003e \u003cspan class=\"n\"\u003ePet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eCat\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eBird\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eCat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eDog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eBird\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThat single line generates a struct implementing \u003ccode\u003eIUnion\u003c/code\u003e with an implicit constructor for each case type. Pattern matching over it looks exactly like a normal \u003ccode\u003eswitch\u003c/code\u003e expression, except the compiler now understands the closed set of possibilities:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epet\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eDog\u003c/span\u003e \u003cspan class=\"n\"\u003ed\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ed\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eCat\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ec\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eBird\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo \u003ccode\u003edefault\u003c/code\u003e arm required. Add a fourth case type to the union declaration later, forget to update this switch, and the compiler warns you. That single behavior, compiler-verified exhaustiveness, is the entire pitch, and it\u0026rsquo;s a legitimate one. Nothing before this gave you that for free.\u003c/p\u003e\n\u003cp\u003eCase types are not limited to concrete classes. Generic type parameters work too, which is where the error-handling use case becomes obvious:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eunion\u003c/span\u003e \u003cspan class=\"n\"\u003eResult\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTError\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eTSuccess\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eTError\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eSome\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eunion\u003c/span\u003e \u003cspan class=\"n\"\u003eOption\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eSome\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eA \u003ccode\u003eResult\u0026lt;TSuccess, TError\u0026gt;\u003c/code\u003e return type forces every caller to handle both the success and error case in a switch, with the compiler flagging anything unhandled. That is the exact shape you were building by hand with a sealed abstract class and two private-constructor subclasses, or with OneOf.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-catch-unions-of-types-not-of-cases\"\u003e\u003ca href=\"/posts/csharp-15-union-types-expectations/#the-catch-unions-of-types-not-of-cases\" title=\"The Catch: Unions of Types, Not of Cases\"\u003eThe Catch: Unions of Types, Not of Cases\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eHere is where I want to slow down, because the enthusiasm around this feature, mine very much included, runs ahead of what it actually is. The \u003ca href=\"https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/union\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eofficial Microsoft Learn documentation\u003c/a\u003e says it plainly: unions in C# are unions \u003cstrong\u003eof types\u003c/strong\u003e, not discriminated or tagged unions in the F# sense.\u003c/p\u003e\n\u003cp\u003eThat distinction matters more than it sounds. In F#, a discriminated union declares its cases and their fields inline as part of the type itself:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fsharp\" data-lang=\"fsharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etype\u003c/span\u003e \u003cspan class=\"nc\"\u003eShape\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003eCircle\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"kt\"\u003efloat\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e|\u003c/span\u003e \u003cspan class=\"n\"\u003eRectangle\u003c/span\u003e \u003cspan class=\"k\"\u003eof\u003c/span\u003e \u003cspan class=\"kt\"\u003efloat\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"kt\"\u003efloat\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThere is no separate \u003ccode\u003eCircle\u003c/code\u003e type floating around your codebase; the case and its data are defined together, once, as part of the union. C#\u0026rsquo;s \u003ccode\u003eunion Pet(Cat, Dog, Bird)\u003c/code\u003e is a union of three pre-existing, independently usable types. That is a real feature, and a useful one, but it is not \u0026ldquo;F# discriminated unions with C# syntax,\u0026rdquo; which is roughly the pitch I would have made myself before reading past the headline. You still design and maintain \u003ccode\u003eCat\u003c/code\u003e, \u003ccode\u003eDog\u003c/code\u003e, and \u003ccode\u003eBird\u003c/code\u003e as ordinary types; the union just wraps them.\u003c/p\u003e\n\u003cp\u003eThe second catch is more concrete and more likely to bite you in production: boxing. The compiler-generated struct behind the one-line \u003ccode\u003eunion\u003c/code\u003e declaration always stores its contents as \u003ccode\u003eobject?\u003c/code\u003e, so any value-type case gets boxed on every access:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Every read of the int or double case boxes it into object?.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eunion\u003c/span\u003e \u003cspan class=\"n\"\u003eMeasurement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003edouble\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eTo avoid that, you do not get a flag or an attribute to opt out. You either abandon the one-line declaration and hand-write the union struct yourself, or keep the \u003ccode\u003eunion\u003c/code\u003e keyword and delegate to a nested \u003ccode\u003eIUnionMembers\u003c/code\u003e interface, writing only the \u003ccode\u003eTryGetValue\u003c/code\u003e overloads. Either way, implementing \u003ccode\u003eIUnion\u003c/code\u003e (directly or through the provider interface) plus a \u003ccode\u003eHasValue\u003c/code\u003e property and a \u003ccode\u003eTryGetValue(out T)\u003c/code\u003e overload per case is the non-boxing access pattern the official docs describe:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[System.Runtime.CompilerServices.Union]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"nc\"\u003eMeasurement\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRuntime\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCompilerServices\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eIUnion\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003e_meters\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003edouble\u003c/span\u003e \u003cspan class=\"n\"\u003e_kilometers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e \u003cspan class=\"k\"\u003ereadonly\u003c/span\u003e \u003cspan class=\"kt\"\u003ebyte\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 0 = none, 1 = meters, 2 = kilometers\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eMeasurement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003e_meters\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eMeasurement\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003edouble\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003e_kilometers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003eobject?\u003c/span\u003e \u003cspan class=\"n\"\u003eValue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"k\"\u003eswitch\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_meters\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_kilometers\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003e_\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"kc\"\u003enull\u003c/span\u003e \u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eHasValue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"p\"\u003e!=\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_meters\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"n\"\u003eTryGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003eout\u003c/span\u003e \u003cspan class=\"kt\"\u003edouble\u003c/span\u003e \u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003evalue\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003e_kilometers\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003e_tag\u003c/span\u003e \u003cspan class=\"p\"\u003e==\u003c/span\u003e \u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSit with that for a second. The entire point of a union type, in most enterprise pitches I have heard, is \u0026ldquo;less boilerplate than OneOf or a hand-rolled Result type.\u0026rdquo; For struct case types where allocation matters, you write almost the same boilerplate back in: it just moves from a third-party package to your own codebase. That is not nothing (a compiler-recognized shape for it is worth something), but it is not the free lunch I was hoping for.\u003c/p\u003e\n\u003cp\u003eThere is also a genuine null footgun for struct unions: if the union\u0026rsquo;s value could be \u003ccode\u003enull\u003c/code\u003e, your switch needs an explicit \u003ccode\u003enull =\u0026gt;\u003c/code\u003e arm, or the compiler will complain. We just spent a decade teaching developers that \u0026ldquo;the compiler will tell you if something can be null\u0026rdquo; is progress. Reintroducing a null-shaped hole in a brand-new type category, even with a diagnostic backstop, is the kind of thing I would happily skim past in a proposal doc and then rediscover six months later in a Slack thread titled \u0026ldquo;why is this switch not exhaustive.\u0026rdquo;\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-underrated-half-closed-hierarchies\"\u003e\u003ca href=\"/posts/csharp-15-union-types-expectations/#the-underrated-half-closed-hierarchies\" title=\"The Underrated Half: closed Hierarchies\"\u003eThe Underrated Half: \u003ccode\u003eclosed\u003c/code\u003e Hierarchies\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eIf I had to bet enterprise budget on one of these two features today, it would not be \u003ccode\u003eunion\u003c/code\u003e. It would be \u003ccode\u003eclosed\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eclosed\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eCash\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eCreditCard\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eLast4\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003erecord\u003c/span\u003e \u003cspan class=\"nc\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003eBankTransfer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eIban\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003ePaymentMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is not a new runtime concept. It is plain classes and records, marked with a modifier that tells the compiler \u0026ldquo;I am declaring the complete set of subtypes, right here, and I want you to enforce it.\u0026rdquo; Switch expressions over a \u003ccode\u003eclosed\u003c/code\u003e hierarchy get the same exhaustiveness diagnostics as \u003ccode\u003eunion\u003c/code\u003e (\u003ca href=\"https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-15.0/closed-hierarchies\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003ethe proposal\u003c/a\u003e cites \u003ccode\u003eCS8509\u003c/code\u003e and \u003ccode\u003eCS8510\u003c/code\u003e for missing arms), but without a new runtime type, without an \u003ccode\u003eIUnion\u003c/code\u003e interface, without boxing questions, and without a null-arm surprise. It is closer to what a lot of teams are already doing with sealed abstract classes and private constructors, just with the compiler finally checking the thing that developer discipline was checking manually.\u003c/p\u003e\n\u003cp\u003eIf your domain models a fixed set of \u003cem\u003ebehaviorally distinct types with their own methods and inheritance\u003c/em\u003e, reach for \u003ccode\u003eclosed\u003c/code\u003e. If it models a fixed set of \u003cem\u003einterchangeable data shapes you switch over\u003c/em\u003e, \u003ccode\u003eunion\u003c/code\u003e is the better fit, boxing caveats included. Expect both idioms to coexist in the same codebase for a while, and expect a debate about which one a given \u003ccode\u003eResult\u0026lt;T, TError\u0026gt;\u003c/code\u003e-shaped return type should use. I do not think that debate resolves cleanly before GA.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"my-expectation-and-the-risk\"\u003e\u003ca href=\"/posts/csharp-15-union-types-expectations/#my-expectation-and-the-risk\" title=\"My Expectation, and the Risk\"\u003eMy Expectation, and the Risk\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eWhat I expect to be genuinely useful:\u003c/strong\u003e the \u003ccode\u003eclosed\u003c/code\u003e modifier, on day one, for teams already modeling state machines and result types with sealed hierarchies. It formalizes a pattern most senior C# developers already reach for manually and gives it real diagnostics. Low migration cost, low conceptual overhead, no new runtime surface.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhat I expect to be overhyped:\u003c/strong\u003e \u003ccode\u003eunion\u003c/code\u003e understood as \u0026ldquo;F# for C# developers.\u0026rdquo; It is not case-level pattern matching with inline fields; it is a compiler-recognized wrapper around existing types, with real allocation trade-offs for value-type cases that require you to hand-write the same non-boxing boilerplate you were trying to escape.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhat could bite enterprise teams specifically:\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTiming.\u003c/strong\u003e This missed C# 14 / .NET 10 entirely. It is targeted at C# 15 / .NET 11, with a November 2026 GA date reported by community sources (notably \u003ca href=\"https://blog.maartenballiauw.be/posts/2026-06-16-discriminated-unions-in-csharp-for-real-this-time/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eMaarten Balliauw\u0026rsquo;s June 2026 write-up\u003c/a\u003e), but that date is not confirmed on an official Microsoft release-schedule page as of this writing. Plan roadmaps around what has actually shipped, not around a date that has not yet appeared on an official release schedule.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDesign churn.\u003c/strong\u003e As of this writing, only 4 of the 8 linked sub-proposals on the working group\u0026rsquo;s \u003ca href=\"https://github.com/dotnet/csharplang/issues/8928\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003echampion issue\u003c/a\u003e are closed (Custom Unions, Nominal Type Unions, the Non-Boxing Access Pattern, and Union Interfaces). Case Declarations, Closed Enums, Closed Hierarchies, and Standard Unions are still open. Core switch and pattern-matching semantics look stable across preview builds, but reflection support and serialization behavior remain unresolved open questions, and an ad hoc/anonymous union syntax (\u003ccode\u003eA or B or C\u003c/code\u003e) is under discussion and not confirmed for the C# 15 GA scope at all. Preview means preview: do not put this in a library\u0026rsquo;s public API surface yet.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIdiom fragmentation.\u003c/strong\u003e OneOf, hand-rolled \u003ccode\u003eResult\u0026lt;T\u0026gt;\u003c/code\u003e types, sealed hierarchies, and now two new first-class options. Pick one direction per codebase and document why, or your code review comments will spend the next two years litigating which pattern a new PR should use.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\n\n\u003ch2 id=\"practical-takeaway\"\u003e\u003ca href=\"/posts/csharp-15-union-types-expectations/#practical-takeaway\" title=\"Practical Takeaway\"\u003ePractical Takeaway\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDo not migrate anything to \u003ccode\u003eunion\u003c/code\u003e today; it is preview software with a fifth of its design questions still open, and the boxing story alone deserves a team discussion before adoption, not after. Do watch \u003ccode\u003eclosed\u003c/code\u003e hierarchies closely: they are the lower-risk piece, they map onto patterns your team likely already uses, and they cost almost nothing to adopt once .NET 11 is generally available. When it does ship, budget time for a \u0026ldquo;which idiom for which domain shape\u0026rdquo; decision inside your team\u0026rsquo;s coding guidelines, not for a wholesale OneOf-to-union migration on day one. The compiler-checked exhaustiveness is real progress. The \u0026ldquo;just like F#\u0026rdquo; framing is not, and that distinction is worth carrying into the next architecture discussion.\u003c/p\u003e\n","date_modified":"2026-08-19T17:06:06+02:00","date_published":"2026-08-19T17:00:00+02:00","id":"https://daily-devops.net/posts/csharp-15-union-types-expectations/","language":"en","summary":"C# 15 preview ships union types with compiler-checked exhaustiveness: a union of existing types, not F#-style discriminated cases, plus a boxing catch.","tags":["dotnet","csharp","csharp15","softwareengineering"],"title":"C# 15 Union Types and Closed Hierarchies in Preview","url":"https://daily-devops.net/posts/csharp-15-union-types-expectations/"}],"language":"en","title":"C# 15 Language Features in Practice on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}