# C# Language Design Meeting for February 9th, 2026

## Agenda

- [Closed hierarchies open questions](#closed-hierarchies-open-questions)
    - [Confirming API shape](#confirming-api-shape)
    - [Blocking subtyping from other languages](#blocking-subtyping-from-other-languages)
    - [Multiple `CompilerFeatureRequired` attributes](#multiple-compilerfeaturerequired-attributes)
    - [Same module restriction](#same-module-restriction)
    - [Permit explicit use of `abstract` modifier](#permit-explicit-use-of-abstract-modifier)
    - [Subtype metadata](#subtype-metadata)

## Quote(s) of the Day

- "I will always call `unions`, `onions`."
- "This class, despite its name, is not `closed`"

## Discussion

### Closed hierarchies open questions

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.

#### Confirming API shape

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#closedattribute

We started by confirming the shape of the API. We may add `interface`s 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.

##### Conclusion

API approved.

#### Blocking subtyping from other languages

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#blocking-subtyping-from-other-languagescompilers

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.

##### Conclusion

The `CompilerFeatureRequired` approach on constructors is approved. No additional constructor accessibility restrictions will be required.

#### Multiple `CompilerFeatureRequired` attributes

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#use-of-multiple-compilerfeaturerequired-attributes

When 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.

##### Conclusion

Emit a `[CompilerFeatureRequired]` attribute for each feature independently.

#### Same module restriction

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#same-module-restriction

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.

##### Conclusion

Approved. The spec will use "same module" language instead of "same assembly."

#### Permit explicit use of `abstract` modifier

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#permit-explicit-use-of-abstract-modifier

A `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.

##### Conclusion

`closed abstract class` will not be allowed. The `closed` modifier implies `abstract`, and specifying both is an error.

#### Subtype metadata

Spec section: https://github.com/dotnet/csharplang/blob/fab1ad040df56d5cfcca0b271b02a20f30e389a6/proposals/closed-hierarchies.md#todo-should-subtypes-be-marked-in-metadata

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.

##### Conclusion

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.
