← Open in the full interactive course (progress tracking, search & more)

Not every class is meant to be a base class. Sealing one is how you say so — on purpose, in writing, enforced by the compiler.

Notice something about nearly every concrete class in lessons 072 and 073's examples — StandardCarrierProcessor, ExpressCarrierProcessor, EmailChannel, SmsChannel. They were all marked sealed, without much comment. That wasn't an accident, and it wasn't just style. It was a deliberate design decision this lesson finally explains:

public sealed class StandardCarrierProcessor : ShipmentProcessor
{
    // ...
}

// Six months later, someone under deadline pressure tries this:
public class ExpediteHackProcessor : StandardCarrierProcessor   //  compiler error
{
    // "I just need to override CalculateShippingCost for one weird customer..."
}
// CS0509: 'ExpediteHackProcessor': cannot derive from sealed type 'StandardCarrierProcessor'

That compiler error isn't a limitation — it's the entire point. The class's author decided, in advance, that StandardCarrierProcessor is a finished, complete implementation, and that any future variation belongs in a new class implementing the same abstraction, not a hack bolted onto this one. sealed is how that decision gets enforced instead of just hoped for.

In this lesson, you'll learn exactly what sealed class and sealed override do, why sealing is a deliberate design act rather than a defensive reflex, how it helps the JIT compiler generate faster code, and a clear set of guidelines for when to seal and when not to.

What Is a Sealed Class or Member?

sealed applied to a class means no other class may inherit from it. sealed applied to an overriding member (a sealed override) means that particular member cannot be overridden again by any class further down the hierarchy — even though the class itself may still be inheritable.

sealed class

sealed override

Both are about the same underlying idea — closing off a specific extension point — but at two different scopes: the whole type, or a single member within an otherwise still-open type.

Why Sealing Is a Deliberate Act, Not a Defensive Reflex

Recall the fragile base class problem from lesson 072: any class open to inheritance is, from that moment on, an extension contract you have to maintain forever. Every public and protected member becomes something a future subclass might depend on in ways you can't predict. Sealing a class is the explicit statement: "I am not offering this as an extension point. This is a finished, self-contained implementation."

This matters because leaving a class unsealed "just in case someone needs to extend it later" isn't actually neutral — it's a real, standing commitment:

Sealing flips the default: a class starts closed, and gets opened to inheritance only when there's a real, deliberate reason to design it as an extension point — exactly the way lesson 072's ShipmentProcessor was designed on purpose, with specific virtual extension points, while its concrete carriers were sealed.

Big Picture — Where Sealing Fits in a Hierarchy

A DELIBERATELY-SHAPED HIERARCHY
ShipmentProcessor (abstract) designed to be extended
StandardCarrierProcessor : ShipmentProcessor sealed
ExpressCarrierProcessor : ShipmentProcessor sealed

This is the shape most production hierarchies should have: one deliberately open abstraction at the top, and sealed, finished implementations at the leaves. Depth doesn't keep growing "just in case" — a new requirement gets a new sealed sibling class, not a deeper chain.

sealed override — Closing One Extension Point Without Closing the Whole Class

public class ReportExporter
{
    public virtual string GetFileExtension() => "txt";
    public virtual string GetMimeType() => "text/plain";
}

public class CsvReportExporter : ReportExporter
{
    // Sealed: the file extension for CSV is a fixed, well-known fact.
    // No further subclass should ever be allowed to redefine what "CSV" means.
    public sealed override string GetFileExtension() => "csv";

    // NOT sealed: MIME type nuances (e.g. a vendor-specific CSV variant)
    // are plausible enough to leave open.
    public override string GetMimeType() => "text/csv";
}

public class ExcelCompatibleCsvExporter : CsvReportExporter
{
    // public override string GetFileExtension() => "csv-x";  //  compiler error — sealed above
    public override string GetMimeType() => "text/csv; charset=utf-8"; //  still open
}

This is the precision tool version of sealing: CsvReportExporter itself stays open to further inheritance, but one specific fact about it — its file extension — is nailed down permanently. This is exactly the "circuit breaker" mentioned in lesson 072: it stops fragility from propagating past this one point, without shutting down the entire hierarchy.

Simple Example

public sealed class Money(decimal amount, string currencyCode)
{
    public decimal Amount { get; } = amount;
    public string CurrencyCode { get; } = currencyCode;

    public static Money operator +(Money a, Money b)
    {
        if (a.CurrencyCode != b.CurrencyCode)
            throw new InvalidOperationException("Cannot add different currencies.");
        return new Money(a.Amount + b.Amount, a.CurrencyCode);
    }

    public override string ToString() => $"{Amount:N2} {CurrencyCode}";
}

// public class DiscountedMoney : Money { }   //  compiler error — Money is sealed

Code → Meaning → Result: Money represents a precise, well-defined value — an amount plus a currency. There is no meaningful "kind of Money" that should be modeled as a subclass; any variation (a discount, a tax adjustment) is a different value, computed and returned as a new Money, not a new subtype. Sealing it documents that decision and prevents anyone from accidentally building a fragile hierarchy on top of a value type that was never meant to have one.

Real-World Example — A Plugin Architecture with Deliberate Sealing

A reporting engine (echoing lesson 073's composed design) defines an extensible base for exporters, but each concrete exporter that ships with the product is sealed — leaving room only for genuinely new exporters, never quiet mutations of existing ones.

public abstract class ReportExporter
{
    protected readonly ILogger Logger;
    protected ReportExporter(ILogger logger) => Logger = logger;

    // Non-virtual: the export pipeline's shape is fixed for every exporter.
    public byte[] Export(ReportData data)
    {
        Logger.Log($"Exporting report '{data.Title}' as {FormatName}...");
        var bytes = Serialize(data);
        Logger.Log($"Export complete: {bytes.Length} bytes.");
        return bytes;
    }

    protected abstract string FormatName { get; }
    protected abstract byte[] Serialize(ReportData data);
}

public sealed class PdfReportExporter(ILogger logger, IPdfRenderer renderer) : ReportExporter(logger)
{
    protected override string FormatName => "PDF";
    protected override byte[] Serialize(ReportData data) => renderer.Render(data);
    // sealed: PDF rendering is a complete, well-tested implementation.
    // A "custom PDF variant" is a new class implementing ReportExporter directly,
    // not a subclass of this one.
}

public sealed class CsvReportExporter(ILogger logger) : ReportExporter(logger)
{
    protected override string FormatName => "CSV";

    protected override byte[] Serialize(ReportData data)
    {
        var csv = string.Join("\n", data.Rows.Select(r => string.Join(",", r)));
        return System.Text.Encoding.UTF8.GetBytes(csv);
    }
}

Why sealing every leaf class here is the right call:

Under the Hood — Sealing and JIT Devirtualization

HOW SEALING HELPS THE JIT PROVE THINGS
1. A VIRTUAL CALL NORMALLY NEEDS A VTABLE LOOKUP
2. DEVIRTUALIZATION SKIPS THE LOOKUP WHEN THE TYPE IS PROVABLY KNOWN
3. A SEALED CLASS MAKES THAT PROOF TRIVIAL
4. THIS IS A REAL BUT SECONDARY BENEFIT — NOT THE MAIN REASON TO SEAL
Worth knowing: the JIT can sometimes devirtualize calls even on unsealed classes, if it can prove the concrete type some other way (e.g. from a local new SomeClass() whose result is never widened). But sealed makes that proof unconditional and guaranteed, rather than dependent on what the JIT happens to be smart enough to infer.

Common Confusion

"sealed means the class can't be changed" — no, it means it can't be inherited from

sealed has nothing to do with mutability, or with whether the class's own source code can be edited. It only affects inheritance: whether another class can appear in an : ClassName position. A sealed class's own methods, fields, and properties behave exactly as declared — sealing purely closes off the one specific relationship of "being a base class for something else."

sealed alone on a class with no base — is it even doing anything?

public sealed class Money { ... }, where Money doesn't inherit from anything else, still does real work: it prevents other classes from inheriting from Money. The keyword's job is always about what can derive from this class, never about what this class itself derives from.

Common Mistakes

Mistake 1 — Sealing a class that was specifically designed to be an extension point

Marking the abstract-style base itself sealed defeats the purpose — abstract and sealed together on the same class is actually a compile error in C#, and even a non-abstract base designed with virtual extension points shouldn't be sealed, or those extension points become meaningless.

Seal the concrete, finished leaves of a hierarchy; leave the deliberately-extensible base classes unsealed.

Mistake 2 — Leaving every class unsealed "by default," out of habit

Never sealing anything means every concrete class in the codebase is silently offering itself as a base class, whether or not that was ever intended — reintroducing the fragile base class risk everywhere, by omission rather than decision.

Default to sealed for concrete classes, and unseal deliberately, the same way you'd deliberately mark a method virtual (lesson 072's Mistake 1) — extension should be an intentional design choice, not an accident of never having typed one extra keyword.

Mistake 3 — Sealing to work around a design problem instead of fixing it

Sealing a class specifically to stop a colleague from subclassing it to "hack in" one extra piece of behavior, without providing any legitimate way to get that behavior in.

If there's a real, recurring need to vary behavior, provide it properly — a constructor parameter, a composed interface (lesson 073), or a genuine virtual extension point — rather than using sealed purely as a lock with no accompanying key.

When to Seal, and When Not To

Seal it

Don't seal it

Rule of thumb: seal by default for internal, concrete implementation classes. Unseal — deliberately, with a documented reason — only when you're genuinely designing an extension point, not merely leaving the door open "in case."

Mental Model

sealed class = "this is a finished implementation. Extend the abstraction, not this class."
sealed override = "this one fact about this class is permanent — everything else can still evolve."

Remember:
· Sealing is about inheritance into the class, never about mutability or editability.
· An unsealed class is a standing extension contract, whether or not anyone has used it yet.
· Devirtualization is a real bonus of sealing — but design clarity is the reason to reach for it.

Key Takeaway


Check Your Understanding

Let's confirm you can reason about sealing as a deliberate design decision, not just a syntax rule.

1. What is the primary design reason to mark a concrete class sealed?

Show answer

Correct: B

Why B is correct: Sealing is fundamentally a design statement about intent — this class was not designed to be extended, so extension is disallowed structurally rather than merely discouraged by convention.

Why A is incorrect: Sealing has no effect on whether fields or properties can be mutated — that's controlled separately, by readonly, property setters, and so on.

Why C is incorrect: Sealing is unrelated to accessibility; a sealed class can still be public, fully visible outside its assembly, just not inheritable.

Why D is incorrect: A sealed class can implement any number of interfaces — sealing only blocks other classes from inheriting from it.

Reinforcement: Sealing is about closing off one relationship — being a base class — not about locking down the class in every other sense.

2. In the CsvReportExporter example, GetFileExtension() is marked sealed override while GetMimeType() is left as a plain override. What does this achieve?

Show answer

Correct: B

Why B is correct: This demonstrates the precision of sealed override — it closes exactly one extension point (the file extension) while the class remains inheritable and another member (the MIME type) stays genuinely open for further overriding.

Why A is incorrect: CsvReportExporter itself is not sealed in this example — ExcelCompatibleCsvExporter successfully derives from it in the code shown.

Why C is incorrect: They behave very differently for further subclasses: attempting to override GetFileExtension() again fails to compile, while overriding GetMimeType() succeeds.

Why D is incorrect: sealed override members remain fully virtual-dispatch instance members for everything above them in the hierarchy — they are not converted to static.

Reinforcement: sealed override lets you close individual extension points without sealing the entire class — a more surgical tool than sealing the whole type.

3. How does marking a class sealed potentially help runtime performance?

Show answer

Correct: B

Why B is correct: Because no subclass of a sealed class can ever exist, the JIT has a guarantee it can act on: any call whose static type is known to be that sealed class must resolve to that exact implementation, which is precisely what devirtualization needs to be safe.

Why A is incorrect: Sealed classes are ordinary reference types and participate in garbage collection exactly like any other class.

Why C is incorrect: Sealing has no bearing on stack versus heap allocation — that distinction is governed by whether a type is a class or struct, and by escape analysis, not by sealed.

Why D is incorrect: There is no threading behavior implied by sealed whatsoever.

Reinforcement: This is the same devirtualization concept introduced in lesson 074's quiz — sealing is what makes it a guarantee rather than a JIT best-effort inference.

4. A team is designing a public library and debating whether to seal HttpRetryPolicy, a concrete class with no current subclasses. What is the most important factor in that decision?

Show answer

Correct: B

Why B is correct: For a public library especially, the decision has real consequences: leaving it unsealed without a deliberate design invites the fragile base class problem across an API boundary you don't fully control, while sealing it after external consumers have already subclassed it would break their code — so the decision should be made deliberately, up front.

Why A is incorrect: Method count has no bearing on whether a class should be an extension point.

Why C is incorrect: Naming conventions don't determine sealing — the deciding factor is intended usage, not the identifier chosen.

Why D is incorrect: This is precisely wrong for a public library: sealing a previously-unsealed class that real consumers have already extended breaks their code — the sealing decision has real, lasting consequences and should be made deliberately.

Reinforcement: For public APIs especially, decide sealing intentionally and early — it's far easier to unseal later than to seal something people already depend on extending.

You now default to sealed the way a careful engineer does — which sets up interfaces (077) perfectly, since interfaces are how you offer real extensibility without ever risking the fragile base class problem at all.


dotnetmadeeasy.com — Learn C# and .NET, the right way.