Welcome back. In our previous lesson, we explored how C# records offer a concise and powerful way to create immutable data transfer objects. We saw that records, by default, provide value-based equality and are effectively sealed unless you explicitly define an inheritance hierarchy. Today, we'll expand on this concept of controlling inheritance.
This lesson will focus on the sealed keyword, a feature you've used but may not have explored in depth. Our goal is to apply sealed classes to prevent inheritance and articulate their design and performance implications. We will move beyond the basic syntax to understand why and when to seal a class, examining it as a deliberate design choice with tangible effects on code maintainability, testability, and runtime performance.
1. The sealed Keyword: A Tool for Control
At its core, the sealed keyword is straightforward: it prevents other classes from inheriting from the class it is applied to. You can also apply it to an override method or property in a derived class to prevent it from being overridden further down the inheritance chain.
The following code demonstrates both uses. The SuperAdminAccount class is sealed, so any attempt to inherit from it, like the commented-out ManagerAccount class, results in a compile-time error. Similarly, the ResetPassword method in AdminAccount is sealed, meaning no class deriving from AdminAccount could provide its own implementation of that method.

While the syntax is simple, the decision to use sealed has significant implications for your design.
2. The Design Philosophy: Sealing by Default?
In C#, classes are inheritable by default. You must explicitly use the sealed keyword to prevent it. However, many experienced developers and even members of the C# language design team argue this should be the other way around: classes should be sealed by default, and you should have to explicitly mark them as open for inheritance.
This "sealed by default" philosophy is not about restricting developers, but about expressing clear intent.
Why all your classes should be sealed by default in C#
Nick Chapsas, in his video "Why all your classes should be sealed by default in C#", provides an excellent overview of this design philosophy. He discusses why it's a good practice, especially for library authors.
Watch the segment from the design philosophy section. He explains the viewpoint that classes should be designed for inheritance intentionally, not just left open by default, and references opinions from prominent C# community figures like Jon Skeet.
When you leave a class open for inheritance, you are implicitly creating a contract. You are telling consumers of your code, "You can extend this class, and I will try not to break your derived classes in the future." If you didn't actually design the class for extension—by providing appropriate virtual members and considering its internal state—you may inadvertently lock yourself into a specific implementation or make future refactoring much more difficult.
Sealing a class makes your intent explicit: "This class is not designed for inheritance." This is a powerful form of encapsulation.
This brings us to a crucial question: when should you seal a class, and when should you leave it open?

Let's break down the points on this scale:
-
Seal it:
- Utility Class: Classes with only
staticmethods (likeMath) have no state to inherit and should be sealed. - Performance-Critical: As we'll see shortly, sealing can give the JIT compiler opportunities to optimize method calls.
- Library Publication: If you are a library author, sealing classes you don't intend for extension prevents consumers from taking dependencies on implementation details you may want to change later.
- Invariant Requirements: If a class maintains complex internal rules or state (invariants) that could be broken by a poorly written subclass, sealing it protects its integrity.
- Utility Class: Classes with only
-
Skip it (Leave it open):
- App-Level Logic & Flexibility Focus: In your own application's business logic, you might favor flexibility, allowing other team members to extend classes as new requirements emerge.
- Testing Importance: This is a major consideration. Many mocking frameworks require classes to be non-sealed to create test doubles. We'll discuss this next.
3. A Critical Trade-Off: Mocking and Unit Testing
For a full-stack developer, unit testing is a daily reality. The decision to seal a class has a direct impact on your testing strategy. Mocking frameworks like Moq or NSubstitute work by generating a new class at runtime that inherits from the type you want to mock and overrides its virtual members.
This means if you try to mock a sealed class, it will fail.
Boosting Performance With Sealed Classes in .NET - Code Maze
The article from Code Maze clearly explains this limitation and the standard architectural solution.
Please read the section titled Mocking With Sealed Classes. It demonstrates the error you'd get from a mocking library and points to the correct design pattern to use instead.
As the article states, the solution is a fundamental principle of good object-oriented design: program to an interface, not an implementation.
Instead of depending on a concrete SealedService, your code should depend on an IService interface.
public interface IUserService
{
void ResetPassword(int userId);
}
// Your service is an implementation detail. It can be sealed.
public sealed class UserService : IUserService
{
public void ResetPassword(int userId)
{
// ... implementation
}
}
// Your consumer class depends on the abstraction.
public class PasswordController
{
private readonly IUserService _userService;
public PasswordController(IUserService userService)
{
_userService = userService;
}
// ...
}
In your unit tests, you can then easily mock the IUserService interface without any issue. This design not only solves the mocking problem but also leads to more decoupled and maintainable code, a key aspect of advanced class handling.
4. The Performance Dimension: Devirtualization and Inlining
For a long time, the performance benefit of sealed classes was considered a myth in the .NET world. However, with modern Just-In-Time (JIT) compilers (starting around .NET Core 2.0 and significantly improved in .NET 6+), the benefits are real and measurable.
The key concept is devirtualization.
Do sealed classes really offer performance benefits? - Stack Overflow
This Stack Overflow thread provides an excellent historical and technical summary of the performance implications.
First, read the top-voted answer's summary, which contrasts the old understanding with the modern reality: The answer is now yes. Next, read the explanation of devirtualization in the second answer: Update: As of .NET Core 2.0. Then, look at the answer confirming this for .NET 6, which shows benchmark results for method calls and type checks: Starting from .NET 6.0. Finally, read the comment that frames sealing as a form of encapsulation, reinforcing the design philosophy we discussed earlier: the best reason to seal a class.
To summarize the technical details:
- When you call a
virtualmethod on a non-sealed class, the JIT compiler emits acallvirtinstruction. At runtime, this requires looking up the object's type to find the correct method implementation in a virtual method table (v-table). This is a small but non-zero overhead. - When the JIT compiler sees a method call on a
sealedclass, it knows there can be no other implementations. It can devirtualize the call, replacing the indirectcallvirtwith a directcallto the method's memory address. - This direct call is faster and, more importantly, it opens the door for another powerful optimization: inlining. The JIT can copy the body of the small, devirtualized method directly into the calling method, eliminating the overhead of the call entirely.
The performance gains apply to more than just method calls. Type checks (is and as) and even array operations can be faster because the runtime doesn't need to check an entire inheritance hierarchy.
Why all your classes should be sealed by default in C#
Nick Chapsas' video provides clear BenchmarkDotNet results demonstrating these performance gains across different scenarios.
First, watch the benchmark for method calls. Note the significant difference between the sealed and open class calls. Next, see the results for type checks and array stores, which also show a clear benefit. Finally, see the practical application of this knowledge with the introduction of the Roslyn analyzer CA1852 from the analyzer demo. This tool can automatically suggest which of your internal classes can be sealed.
While these are nanosecond-level optimizations, they add up in performance-critical code (hot paths) and are a key reason why the .NET team themselves now seal almost all new internal classes in the runtime libraries.
Conclusion
In this lesson, we've moved beyond the syntax of sealed to understand its role as a important tool for class design. You now have a framework for deciding when and why to apply it.
Here are the key takeaways:
- Sealing Expresses Intent: More than a performance tweak,
sealedis a design statement. It declares that a class was not designed for inheritance, preventing misuse and making your API contracts clearer. Adopting a "seal by default" mindset for classes not explicitly designed for extension is a modern best practice. - Design Has Trade-Offs: Sealing a class can complicate unit testing with mocking frameworks. This reinforces the importance of programming to an interface, which resolves the issue while promoting a more decoupled architecture.
- Performance is a Consequence of Design: In modern .NET, sealed classes enable the JIT compiler to perform significant optimizations like devirtualization and inlining. This leads to measurable performance improvements in method calls, type checks, and other operations, especially in frequently executed code.
- Tooling Can Guide You: Roslyn analyzers like
CA1852can help you identify opportunities to apply thesealedkeyword in your codebase, making it easier to adopt these best practices.
In our next lesson, we will explore another powerful feature for designing expressive types: operator overloading. You will learn how to define custom behavior for operators like +, -, ==, and != to make your domain-specific classes more intuitive to use.