Skip to content

Compile probe: C# 15 closed hierarchies and union types for the five variants, on net10.0 and net11.0 #148

Closed

Description

@CaffeinatedCoder

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:

  1. Does a closed abstract record with five nested sealed records compile for net10.0 with LangVersion 15? Does it need any attribute or runtime type that net10.0 lacks?
  2. Same for a union over the five variants, with UnionAttribute/IUnion declared in-project when the target lacks them.
  3. For a consumer switch that already has a _ => throw arm 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.
  4. Is a closed hierarchy's guarantee assembly-scoped only (derivation refused outside the declaring assembly)? If so it is weaker than the private constructor and both must stay.
  5. Does System.Text.Json in .NET 11 serialize a union the way RangeJsonConverter writes 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

  1. added this to the 11.0.0 milestone on Oct 18, 2026
  2. added
    choreBuild, CI, packaging, refactoring with no behaviour change
    area/rangesRange unions, variants and the shape engines
    package/nodatimeAffects CodoMetis.ValueRanges.NodaTime
    package/coreAffects CodoMetis.ValueRanges
    on Oct 18, 2026
  3. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Partial result from an independent review (GPT 6 Astra, 2026-10-18, .NET 11 RC1 SDK): a standalone closed hierarchy with sealed variants compiled a switch without a fallback arm, and removing an arm produced CS8509 naming the missing variant. That settles question 1 for net11.0. Still open here: the net10.0 target, question 3 (whether an existing _ => throw arm becomes an error), and question 4 (assembly scoping).

    Two further notes from that review to fold into the decision: native union structs have a different representation and default-value behaviour from the records, so they are not a drop-in modernization and closed is the natural first investigation; and a .NET 11 EF adapter does not by itself require the core value types to target net11.0, so if closed compiles for net10.0 the core packages of the 11 line could keep that TFM for reach while the EF packages move. Decide that here too.

  4. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Probe results (2026-10-18, SDK 11.0.100-rc.2.26504.105, LangVersion preview)

    One library with a closed record IntRange (five nested sealed records, private constructor) and a union IntRangeUnion(...) over them; a consumer assembly; an outsider assembly; built for net10.0 and net11.0.

    Question Result
    1. closed on net10.0 Compiles, with an in-project System.Runtime.CompilerServices.IsClosedTypeAttribute under #if !NET11_0_OR_GREATER (CS0656 without it). closed is implicitly abstract: writing abstract closed is CS9384. No runtime support: the code ran on .NET 10.0.12 and 11.0.0.
    2. union on net10.0 Compiles, with in-project UnionAttribute and IUnion (CS0518/CS0656 without them). Exhaustive switch over the cases, no discard. Runs on both runtimes.
    3. Existing _ => throw arm next to complete arms No warning, no error, in the library and in a consumer. The rewrite breaks nothing for consumers who keep their discards.
    3b. Missing arm CS8509 naming the missing variant, a warning (an error under TreatWarningsAsErrors).
    4. Deriving outside the assembly CS9382 error. The guarantee is assembly-scoped, as suspected; the private constructor stays as the in-assembly guard.
    5. Cross-asset A net11.0 consumer compiling against the net10.0 asset gets exhaustiveness and CS8509 from the internal polyfilled attribute in metadata.

    What follows

    • closed is 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 with LangVersion preview, so the 10.x minor that adds closed waits for .NET 11 GA, when LangVersion 15 is stable; until then it lives on net11 as 11.0.0-preview.2. Adding the attribute is not an ApiCompat break (verify at pack).
    • Recommendation for Mark the range unions closed (C# 15) for compile-time exhaustiveness, on both lines #149: closed, not union. The range unions already are closed reference-type hierarchies; a native union would change the representation (boxed object? value, different default) and every consumer's declared type, for no gain in exhaustiveness. closed on the eleven union types plus RangeSet where a shape switch exists, the polyfill in each shipping assembly, and the engines unchanged.
    • Consequence for the vision (epic Epic: 11.0.0 #147): the "surface differs per line" argument for the core packages weakens: with closed on 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 keep net10.0 (the probe shows a net11.0 consumer is fine with it) or move to net11.0 is 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 only net11.0 gives: System.Text.Json's union support in .NET 11 recognizes the BCL UnionAttribute, not a polyfill, which matters only if union were 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.

  5. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Decided 2026-10-18: closed, not union, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/rangesRange unions, variants and the shape engineschoreBuild, CI, packaging, refactoring with no behaviour changepackage/coreAffects CodoMetis.ValueRangespackage/nodatimeAffects CodoMetis.ValueRanges.NodaTime

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions