Sealed by Default in .NET: when it Shines and when it Bites
Sealed looks like a tiny keyword, but it carries serious weight in modern .NET. This post explains what sealed does at class and member level, why the JIT can make sealed code faster, when you should not seal, and how to keep code testable with interfaces. You will see small, runnable examples and practical patterns that balance performance, clarity, and flexibility.
Sealed in C#: safer designs and sneaky speed gains
You know that one drawer at home labeled Do not open because pasta will avalanche onto your feet? Sealing a class is like labeling that drawer in your codebase. It sets expectations. It reduces surprise. And in modern .NET it often makes your code faster. That is a tidy trifecta future you will thank you for.
In this tour we will:
- Demystify what the sealed modifier does at class and member level
- See why the JIT cares and how it squeezes out performance
- Call out the times you should not seal
- Land on practical patterns that keep your code testable and EF friendly
What sealed actually means
Sealed prevents inheritance from spreading where it should not. At the class level, no one can derive from it. At the member level, only an overridden member can be sealed, which stops further overrides.
Let us start with a friendly class-level example. I promise no pasta.
public sealed class DeathStarConfig{ public string Endpoint { get; init; } = "https://laser.empire"; public int CooldownSeconds { get; init; } = 42;}You cannot derive from DeathStarConfig. That intent is crystal clear for maintainers, code analyzers, and the JIT.
Member-level sealing is handy when you do allow inheritance but want to lock a specific behavior.
public class Wizard{ public virtual int Cast() => 1;}
public class GreyWizard : Wizard{ public sealed override int Cast() => 2;}GreyWizard can be inherited, but its Cast behavior is final. If a later subclass tries to override Cast again, the compiler objects. Your design wins.
Why the JIT smiles when you seal
Virtual calls require the runtime to figure out which implementation to run. That lookup is cheap, but not free. When a class is sealed, or an override is sealed, the JIT can devirtualize the call and often inline the method. That removes the lookup and the call overhead. It also opens doors for additional optimizations like constant propagation and dead code elimination.
Here is a tiny microbenchmark to compare a virtual call vs a nonvirtual call. Your numbers will vary by machine, runtime, and weather in Mordor, but the shape is consistent.
using System.Diagnostics;
var wiz = new GreyWizard();var sw = Stopwatch.StartNew();int total = 0;for (int i = 0; i < 5_000_000; i++) total += wiz.Cast();sw.Stop();Console.WriteLine($"Virtual-ish time: {sw.ElapsedMilliseconds} ms");Now compare with a sealed class that exposes a nonvirtual method.
using System.Diagnostics;
public sealed class Paladin { public int Smite() => 2; }var pal = new Paladin();var sw2 = Stopwatch.StartNew();int total2 = 0;for (int i = 0; i < 5_000_000; i++) total2 += pal.Smite();sw2.Stop();Console.WriteLine($"Nonvirtual time: {sw2.ElapsedMilliseconds} ms");On modern .NET you will usually see the sealed or nonvirtual version win. The exact delta depends on surrounding code and whether the JIT can inline the call. Even small savings add up in hot paths.
Bonus: type checks like is and as can be faster when the hierarchy is closed, because the runtime has fewer possibilities to consider.
Defensive design with clear intent
Beyond speed, sealed is about contracts. It communicates that a type is meant to be used as-is. This reduces the chance that someone overrides behavior you depend on for business invariants.
I like to pair sealed with interfaces so I get extensibility where it matters without risking inheritance surprises.
public interface IHyperdrive { bool Jump(); }
public sealed class Falcon : IHyperdrive{ public bool Jump() => true;}Consumers depend on IHyperdrive. The concrete Falcon is locked down for safety and performance.
When not to seal
There are important exceptions. Sealing everything without thinking can paint you into a corner faster than Dwight alphabetizing the kitchen.
- EF Core lazy loading: Proxies require virtual members and a nonsealed entity so the framework can create a derived type at runtime.
- Dynamic mocking libraries: Most mocking tools generate subclasses at runtime. Sealed makes that impossible.
Here is an EF flavored example that should not be sealed if you lean on lazy loading.
public class Quest{ public int Id { get; set; } public virtual List<Loot> Items { get; set; } = new();}
public class Loot { public string Name { get; set; } = "Ring"; }If Quest were sealed, EF Core could not weave in its proxy to intercept Items.
For testing, favor interfaces or delegates so your tests can swap implementations without subclassing concrete classes.
public interface IPortalService { Task<bool> OpenAsync(); }
public sealed class StrangePortal : IPortalService{ public Task<bool> OpenAsync() => Task.FromResult(true);}Your test doubles can implement IPortalService directly. No inheritance tricks needed.
Method-level sealed can be a sweet spot
Sometimes you need inheritance for structure, but you still want hot methods to be stable and fast. Sealing the override helps the JIT and guards intent.
public class Boss{ public virtual string Roar() => "meh";}
public class MiniBoss : Boss{ public sealed override string Roar() => "RAWR";}Callers that see a Boss reference still benefit. The JIT can often prove that Roar resolves to MiniBoss.Roar and optimize accordingly.
Practical guidance that ages well
- Prefer sealed for concrete types that represent behavior, value, or configuration
- Expose capability through interfaces for extensibility and testing
- Seal overridden hot-path methods when the behavior is not supposed to vary
- Keep EF Core entities open if you rely on lazy loading or proxies
- For testing, mock interfaces or use constructors that accept delegates or simple abstractions
Here is a tiny composition example that keeps things flexible and fast.
public interface ISpell { int Cast(); }
public sealed class Fireball : ISpell { public int Cast() => 10; }
public sealed class Mage{ private readonly ISpell _spell; public Mage(ISpell spell) => _spell = spell; public int Attack() => _spell.Cast();}Mage is sealed, its hot path Attack can be inlined, and tests can pass in a dummy ISpell without subclassing.
A simple checklist
- Does someone need to derive from this type in production code? If not, seal it
- Do you rely on EF Core proxies or runtime subclassing? If yes, do not seal that entity
- Do your tests need to mock it? Extract an interface and seal the concrete type
- Is this an overridden method that must not change? Seal the override
Wrap up
Sealed is a small keyword with big ripple effects. It communicates intent, protects invariants, and helps the JIT produce tighter code. Use it by default for concrete implementations, open types intentionally where frameworks or tests need it, and pair it with interfaces to keep your designs flexible. Your future self and your CPU caches will both sleep better.