[Proposal question]: Should the immediate descendants of a `closed` type be required to have the same accessibility as the base?
- Champion: https://github.com/dotnet/csharplang/issues/9499
- Spec section: Exhaustiveness when a subtype can't be used
- Prior LDM decision (2026-04-20): Accessibility and exhaustiveness
- Related: dotnet/runtime#129041 (STJ support), dotnet/runtime#129009 (
[IsClosedType])
The problem
A closed class restricts derivation to its declaring assembly so that a switch over its direct descendants can be treated as exhaustive. Today a direct subtype may be less accessible than the closed base, in which case a consumer that can see the base but not the subtype can never write an exhaustive switch:
// Library assembly
public closed class Shape { }
public sealed class Circle : Shape { }
internal sealed class Hidden : Shape { } // less accessible than the public base// Consumer assembly
string Describe(Shape shape) => shape switch
{
Circle => "circle",
// warning: switch is non-exhaustive. Pattern 'Shape' is not handled.
// But 'Hidden' is internal to the library and cannot be named here, so the consumer
// can never exhaust the switch by matching the derived types.
};The current proposal accepts this and reports it only at the use site (the spec section linked above). LDM considered a declaration-time accessibility restriction on 2026-04-20 and declined: "We will not add any new restrictions related to accessibility."
The question
Should we revisit that decision and require, at declaration time, that every immediate descendant of a closed type have the same declared accessibility as the base?
Proposed rule
Every immediate descendant (direct subtype) of a
closedtype must be declared with the same declared accessibility as the closed base type; any other accessibility is a compile-time error.
public closed class Shape { }
public sealed class Circle : Shape { } // ok: same accessibility as 'Shape'
internal sealed class Hidden : Shape { } // error: a direct subtype of a closed type must have
// the same accessibility as its baseConsiderations in favor
- Exhaustiveness then holds for every consumer that can see the base — which is the point of
closed. - A declaration-time error is actionable at the hierarchy, versus a non-exhaustiveness warning in distant (possibly cross-assembly) consumer code that the consumer cannot fix.
- The direct descendants are part of the type's contract; hiding cases from consumers who can see the base contradicts what
closedadvertises. - STJ's closed-hierarchy support (
JsonSerializerOptions.InferClosedTypePolymorphism, runtime#129041) infers a$typediscriminator (nameof(DerivedType)) for every derived type recorded in[IsClosedType(DerivedTypes = …)](runtime#129009); permitting less-accessible descendants means an untrusted payload could instantiate an otherwise-inaccessible type/constructor — a potential security concern, which STJ is likely to decline to support regardless.
Scope and related considerations
file-local closed classes.fileis a separate modifier, not a named accessibility level, so it needs explicit treatment: afileclosedbase should requirefile-local direct subtypes (and a more-visible base cannot have afilesubtype). The current implementation has not exercisedfile closed class; that gap needs coverage regardless of the outcome here.- Unspeakable or constraint-adding generic subtypes. The spec's Exhaustiveness when a subtype can't be used already notes that a generic subtype can be unspeakable for a given base instantiation — e.g.
class D2<V> : C<V[]>underclosed class C<T>, where noD2<…>pattern can be written for an existentialC<X>. The same reasoning behind this question applies: guaranteeing a usable construction wherever any base construction is used would mean disallowing such non-speakable derived generics at declaration, effectively restricting generic subtypes to "trivial" generics — ones that pass their type parameters directly to the base and add no constraints of their own. Best treated as a separate question. - Consumption-site dependency on inaccessible metadata. Reference assemblies and trimming tools would have to special-case descendants of a
closedclass to keep consumer pattern matching correct, and Roslyn metadata-import modes that hide inaccessible members interact poorly with consumption-site behavior that depends on inaccessible derived types. This is the same class of hole already known from private struct fields (managed/unmanaged determination — and thus pointer eligibility — depending on inaccessible fields); we would prefer not to enlarge it. The rule removes the dependency. - Nested
closedlevels. The rule applies to eachclosedtype's own direct descendants independently; deeper hierarchies stay matchable. - Type parameters constrained to a
closedtype. Unaffected — that concerns generics and exhaustiveness, not accessibility.
Options
- Adopt the rule as a compile-time error — strongest guarantee; a
closedtype is always exhaustively matchable by every consumer that can see it. - Adopt the rule as a warning — flags the hazard at declaration without hard-breaking the pattern; weaker guarantee.
- Status quo (2026-04-20) — no declaration-time restriction; continue reporting non-exhaustiveness only at the use site.
Sub-decision for options 1 and 2: whether the rule extends to require matching file-locality, as described under Scope and related considerations.
Source: dotnet/csharplang