meetings/2026/LDM-2026-02-09.md
unions, onions."closed"Champion issue: https://github.com/dotnet/csharplang/issues/9499
Spec: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md
Today we went through the open questions in the closed hierarchies spec.
We started by confirming the shape of the API. We may add interfaces as a supported target in the future, but for now limiting this simplifies the
specification. Otherwise, we like the shape, modulo getting it approved by the libraries API review.
API approved.
The spec proposes applying [CompilerFeatureRequired("ClosedClasses")] to all constructors of closed classes. Since closed classes are abstract,
their constructors can only be invoked from constructor initializers in derived types. Compilers that do not understand the feature will see the
CompilerFeatureRequired attribute and block derivation, effectively preventing unauthorized subtyping from older C# compilers or other .NET
languages. The [Obsolete] attribute that was used alongside CompilerFeatureRequired for required members is no longer considered necessary,
since CompilerFeatureRequired has been around long enough that compilers are expected to understand it.
The LDM approved this approach. There was also a suggestion about whether constructors of closed types should be required to have
private protected accessibility, to further express the intent that these constructors are only for use by known subtypes. However, this was
considered more trouble than it is worth: private protected is a niche accessibility, primary constructors pick up the accessibility of their
containing type, and synthesized constructors would need special handling. Requiring more restrictive accessibility would add complexity without
meaningful benefit, since CompilerFeatureRequired already blocks unauthorized use. The LDM decided to proceed without constructor accessibility
restrictions and keep an eye on it going forward.
The CompilerFeatureRequired approach on constructors is approved. No additional constructor accessibility restrictions will be required.
CompilerFeatureRequired attributesWhen a closed class also has required members, both [CompilerFeatureRequired("RequiredMembers")] and
[CompilerFeatureRequired("ClosedClasses")] end up on the constructors. The question was whether to emit only the most recent attribute, on
the theory that a compiler supporting closed classes must already support required members.
The LDM concluded that all attributes should be emitted. Each part of the compiler that checks for a given feature can remain independent, without needing to know about other features. It is also a valid scenario for a language or compiler to support one feature but not another; for example, a language might support closed hierarchies but not required members. Users also get better diagnostics when all relevant attributes are present, rather than receiving confusing errors about only one of the features. While the number of attributes could grow over time, that concern can be revisited if it ever actually becomes a problem.
Emit a [CompilerFeatureRequired] attribute for each feature independently.
The proposal strengthens the "same assembly" restriction to "same module," since the compiler supports loading dependencies with multiple modules in a single assembly. The reasoning is the same as for cross-assembly subtyping: the original module needs to know the complete set of subtypes for exhaustiveness checking, and allowing subtypes from another module would break that.
In practice, this distinction is largely academic since modern .NET no longer supports multi-module assemblies. The restriction effectively changes nothing for real-world users, but it is the correct formulation from the language specification perspective, which generally talks about programs in terms of modules.
Approved. The spec will use "same module" language instead of "same assembly."
abstract modifierA closed class is implicitly abstract. Should the language also permit writing closed abstract class?
Arguments for allowing it centered on consistency with how the language handles default accessibility: when there is a default, C# typically
allows you to state it explicitly for clarity. Partial declarations were raised as a scenario where one part of a type might have closed and
another might have abstract, and requiring the abstract to be removed would be unnecessary friction. Additionally, unlike abstract on
interfaces (which is truly meaningless), abstract on a closed class does have a concrete meaning: it is part of the definition of closed.
Arguments for blocking it drew an analogy to static classes, which are emitted as abstract sealed types but do not allow either abstract or
sealed as explicit modifiers. The key difference from the default accessibility case is that there is no alternative here: you cannot make a
closed class non-abstract, so stating abstract is purely redundant rather than picking from multiple options. Blocking the combination also
ensures that every closed class declaration looks the same, which reduces cognitive overhead. A point was also raised about modern coding agent
workflows: languages that define a single uniform way to write things tend to produce better agent experiences, as agents are more likely to
produce consistent output when there is exactly one way to express something.
After heavy discussion, we have decided to move forward with blocking for now. Starting restrictive is the prudent approach: if a compelling
scenario arises where allowing abstract alongside closed is important, the restriction can always be relaxed later.
closed abstract class will not be allowed. The closed modifier implies abstract, and specifying both is an error.
The spec raised whether the compiler should emit an attribute on the closed type listing all of its direct subtypes (e.g.,
[ClosedSubtype(typeof(Subtype1), typeof(Subtype2), ...)]), so that consuming tools do not have to scan the assembly to discover them. From the
compiler's perspective, this is not needed: the typedef table in the assembly metadata has an "extends" column, and scanning it for subtypes of a
given type is fast. Even for large assemblies like Microsoft.CodeAnalysis.CSharp, the typedef table only has around 1,400 entries.
However, concerns were raised about scenarios beyond the compiler. Runtime discovery via reflection would be significantly more expensive and is also incompatible with trimming and AOT, since scanning an assembly for subtypes could return different results depending on what types the trimmer kept. Without breadcrumbs in metadata, closed hierarchies would be effectively a compiler-only feature with no affordable way for runtime tools to discover the hierarchy.
The LDM concluded that this is not a language-level concern. The compiler does not need it, and there are no pending requests from runtime, reflection, or serialization teams. If such requests arise, the compiler can emit additional metadata without changing language-level semantics. For now, subtypes will be discovered by scanning the assembly metadata tables.
No subtype listing attribute will be emitted at this time. The compiler team will respond to requests from runtime or tooling teams if they need cheaper runtime discovery.