‘Hierarchy’ is one of those words that for some reason I need to stop and think about how it is spelled - does it follow the “i after e except after c” or is it an exception? I think I got it right this time.

So enough of spelling, let’s look at another new language feature introduced in C# 15 - Closed Hierarchies. By using the closed modifier, we can now indicate that a class (including record classes) is a “closed hierarchy”. Given this closed type:
public closed record Season
{
public virtual int Rainfall { get; set; }
}
We can then declare other subtypes of that class in the same assembly in the usual way:
public record Winter : Season;
public record Summer(int BushfireRisk) : Season;
public record Spring(int PollenCount) : Season;
public record Autumn : Season;
If we use these types in a switch expression, then the compiler knows all the possible types and can tell if the expression covers all the direct descendants.
static string SummariseSeason(Season season) => season switch
{
Summer s => $"It's summer. Bushfire alert level {s.BushfireRisk}",
Winter w => "It's winter",
Spring sp => $"It's spring. Pollen count {sp.PollenCount}",
Autumn a => "It's autumn",
};
The above code doesn’t need a default case. Creating a Summer instance of Season and calling that method like this:
Season season = new Summer(5);
Console.WriteLine(SummariseSeason(season));
Outputs: It's summer. Bushfire alert level 5
Note that if Season was nullable, then you would need to handle the null case.
Implications of closed types
You can’t inherit from that type in another assembly
If this code was declared in a different assembly to the closed type, it will not compile:
// Invalid code
public record ChickenSalt : closed_base.Season
{
}
Fails with compiler error:
error CS9382: 'ChickenSalt': cannot use a closed type 'Season' from another assembly as a base type.
The closed class is implicitly abstract
You can’t instantiate a closed type directly:
// Invalid code
var class1 = new Season();
Fails with:
error CS0144: Cannot create an instance of the abstract type or interface 'Season'
Closed is different to sealed
The sealed modifier can be applied to a class, or a method or property that overrides a base class’s virtual method or property.
Once something is sealed, it cannot be inherited from or overridden in a subtype.
A subtype of a closed type defaults to being open
A subtype of a closed type is not also closed automatically. This means you can inherit from the subtype in another assembly. This code is valid if it was in another assembly:
public record BushfireRisk : closed_base.Summer
{
}
A subtype can be closed only if you also add the closed modifier to it.
The compiler will warn you about incomplete switch expressions
If you had the following code:
static string IncompleteSummariseSeason(Season season) => season switch
{
Summer s => $"It's summer. Bushfire alert level {s.BushfireRisk}",
Winter w => "It's winter",
Spring sp => $"It's spring. Pollen count {sp.PollenCount}"
};
The compiler will emit the following warning:
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'closed_base.Autumn' is not covered
Using closed can be a breaking change
Adding closed to an existing class makes it implicitly abstract. This could break code that instantiates it or inherits from it in another assembly.
Adding a new subtype to an existing closed hierarchy could also impact downstream consumers; For example, any existing switch expressions they had could now be non-exhaustive and will emit compiler warnings.
Conclusion
Using closed hierarchies allows the compiler to reason about switch expressions and generate warnings if you’ve missed a case.
Full code samples can be found in this repo.
Also, I think after writing this post, I’m getting better at spelling ‘hierarchy’ 😀