Repository navigation
Compile probe: C# 15 closed hierarchies and union types for the five variants, on net10.0 and net11.0 #148
Description
The five sealed variants with a private constructor are a hand-rolled discriminated union: C# cannot prove a switch over them exhaustive, so every switch carries a throwing _ arm and EngineDispatchConventionTests polices it. C# 15 ships closed classes (a switch over the direct descendants needs no default arm) and union types (union Pet(Cat, Dog);, exhaustive switches, value-type cases box, runtime UnionAttribute/IUnion since .NET 11 Preview 5). Microsoft's C# 15 page (2026-09-11) and the union reference (2026-08-14) are the sources; nothing documents either feature on a net10.0 target.
Status 2026-10-18: question 1 is answered for net11.0 by an independent probe on the RC1 SDK: a closed hierarchy's switch compiles without a fallback, and a missing arm is CS8509 naming the variant. Open: net10.0, question 3 and question 4. Two more points for the decision: native union structs have a different representation and default-value behaviour from the records, so closed is the natural first investigation; and if closed compiles for net10.0, the core packages of the 11 line could keep that TFM for reach while only the EF packages move to net11.0.
Before the rewrite is designed, a one-file probe on the .NET 11 RC2 SDK must answer, with the compiler output pasted here:
- Does a
closed abstract recordwith five nested sealed records compile fornet10.0withLangVersion15? Does it need any attribute or runtime type thatnet10.0lacks? - Same for a
unionover the five variants, withUnionAttribute/IUniondeclared in-project when the target lacks them. - For a consumer switch that already has a
_ => throwarm over a closed hierarchy: is the arm now a warning, an error (unreachable pattern), or silent? This decides whether the rewrite breaks every consumer switch once or twice. - Is a
closedhierarchy's guarantee assembly-scoped only (derivation refused outside the declaring assembly)? If so it is weaker than the private constructor and both must stay. - Does
System.Text.Jsonin .NET 11 serialize aunionthe wayRangeJsonConverterwrites a variant today, or does the converter stay?
Outcome: a decision comment naming closed, union, or neither, and whether the rewrite can also ship on the 10.x line (only if the feature compiles on net10.0 without runtime support). The rewrite issue is blocked on this.
Only the .NET 10 SDK is installed on the workstation where this was researched; the probe needs dotnet-sdk 11.0.100-rc.2.
Activity
CaffeinatedCoder commentedon Oct 18, 2026
Partial result from an independent review (GPT 6 Astra, 2026-10-18, .NET 11 RC1 SDK): a standalone
closedhierarchy with sealed variants compiled a switch without a fallback arm, and removing an arm produced CS8509 naming the missing variant. That settles question 1 fornet11.0. Still open here: thenet10.0target, question 3 (whether an existing_ => throwarm becomes an error), and question 4 (assembly scoping).Two further notes from that review to fold into the decision: native
unionstructs have a different representation and default-value behaviour from the records, so they are not a drop-in modernization andclosedis the natural first investigation; and a .NET 11 EF adapter does not by itself require the core value types to targetnet11.0, so ifclosedcompiles fornet10.0the core packages of the 11 line could keep that TFM for reach while the EF packages move. Decide that here too.CaffeinatedCoder commentedon Oct 18, 2026
Probe results (2026-10-18, SDK 11.0.100-rc.2.26504.105,
LangVersionpreview)One library with a
closed record IntRange(five nested sealed records, private constructor) and aunion IntRangeUnion(...)over them; a consumer assembly; an outsider assembly; built fornet10.0andnet11.0.closedonnet10.0System.Runtime.CompilerServices.IsClosedTypeAttributeunder#if !NET11_0_OR_GREATER(CS0656 without it).closedis implicitly abstract: writingabstract closedis CS9384. No runtime support: the code ran on .NET 10.0.12 and 11.0.0.uniononnet10.0UnionAttributeandIUnion(CS0518/CS0656 without them). Exhaustive switch over the cases, no discard. Runs on both runtimes._ => throwarm next to complete armsTreatWarningsAsErrors).net11.0consumer compiling against thenet10.0asset gets exhaustiveness and CS8509 from the internal polyfilled attribute in metadata.What follows
closedis not platform-bound. It needs the C# 15 compiler to build the library and nothing at runtime, so it can ship on both lines. The one constraint: a stable package must not ship withLangVersionpreview, so the 10.x minor that addsclosedwaits for .NET 11 GA, whenLangVersion15 is stable; until then it lives onnet11as11.0.0-preview.2. Adding the attribute is not an ApiCompat break (verify at pack).closed, notunion. The range unions already are closed reference-type hierarchies; a nativeunionwould change the representation (boxedobject?value, different default) and every consumer's declared type, for no gain in exhaustiveness.closedon the eleven union types plusRangeSetwhere a shape switch exists, the polyfill in each shipping assembly, and the engines unchanged.closedon both lines, 11.x core and 10.x core have the same shape. The two lines stay justified by EF Core 11 / Npgsql 11 for the EF packages and by the shared version across the four packages. Whether the 11.x core and NodaTime packages keepnet10.0(the probe shows anet11.0consumer is fine with it) or move tonet11.0is now a reach-vs-simplicity choice for 11.0.0-preview.1: retarget main to net11.0 / EF Core 11 / Npgsql 11 after the v10.0.0 tag, with the four behaviour changes as named tests #103, not a feature choice. One thing onlynet11.0gives: System.Text.Json's union support in .NET 11 recognizes the BCLUnionAttribute, not a polyfill, which matters only ifunionwere adopted.Probe source:
scratchpad/probe148(lib, consumer, outsider, consumer11, run10) in the research session; not committed. Decision is the maintainer's; this issue closes once #149 and #103 carry it.CaffeinatedCoder commentedon Oct 18, 2026
Decided 2026-10-18:
closed, notunion, on both lines (10.x as a minor after .NET 11 GA). #149 carries the design and #103 the core-TFM choice for the 11 line. Probe results are in the comment above.