📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What does sealed mean in C#, and should application classes be sealed by default? Explain sealed classes and sealed overrides, the performance benefits, the effect on testing and mocking, and the versioning rules.

Sealed classes and members in C#: what they prevent, why sealing helps the JIT and type checks, CA1852, testing trade-offs, and why unsealing is safe but sealing is breaking.

Mid-Level Theory Intermediate api-design csharp oop performance sealed testing

🎯 Key points

  • A sealed class cannot be inherited; a sealed override cannot be overridden by deeper subclasses
  • Sealing documents that a type was not designed for extension and protects its invariants
  • The JIT can devirtualise and inline calls on sealed types
  • Type checks and casts to a sealed class are a single method-table comparison
  • Structs, enums, delegates and static classes are implicitly sealed
  • CA1852 flags internal types that can be sealed; seal by default in application code
  • Mocking libraries cannot subclass sealed types, so put interfaces at real I/O boundaries instead
  • Unsealing a public class is non-breaking; sealing a published class is breaking

⏱ 30-second answer

A sealed class cannot be inherited, and a sealed override cannot be overridden further. Sealing says "this type was not designed for extension", which protects its invariants. It also helps performance: the JIT can devirtualise and inline calls on sealed types, and type checks and casts to a sealed class are a single method-table comparison. Structs and static classes are already sealed. Analyser CA1852 suggests sealing internal types with no subtypes. The trade-off is that classic mocking libraries cannot mock sealed classes, which is a hint to depend on an interface at that seam. Unsealing later is non-breaking; sealing a public class later is breaking.

🗣 Full answer (senior)

How I would answer this in an interview

sealed on a class means no class can derive from it; sealed override on a member means no further overrides below that point. My view: yes, application classes should be sealed by default and unsealed only when you deliberately design for inheritance. I'd justify it with design, performance, testing and versioning arguments.

1. Sealed classes and sealed overrides

public sealed class OrderService(IOrderRepository repo) { ... }   // can't be inherited

public class Report
{
    public virtual string Render() => "...";
}
public class SalesReport : Report
{
    public sealed override string Render() => "...";   // subclasses of SalesReport can't override Render
}

Static classes are implicitly sealed (and abstract). Records can be sealed (sealed record), which also simplifies their generated equality. Structs are always sealed.

2. Why seal by default

Design and correctness

  • Inheritance is a contract: once someone subclasses your type, every protected and virtual member, and the order in which you call them internally, becomes something you can't freely change (the fragile base class problem).
  • A class not designed for inheritance usually doesn't document self-use or invariants for subclasses; subclassing it is fragile.
  • Sealing communicates intent: "use me, compose me, don't extend me".
  • Unsealing later is non-breaking; sealing later is breaking. So the safe default is sealed.

Performance

  • The JIT can devirtualise calls on sealed types: virtual calls (including ToString, Equals, interface calls) on a variable of a sealed type become direct calls that can be inlined.
  • Type checks and casts are cheaper: obj is SealedType is a single MethodTable comparison, no hierarchy walk.
  • Array stores of sealed element types can skip covariance checks.
  • The .NET runtime team measured noticeable gains from sealing internal types, and analyser CA1852 ("Seal internal types") was added in .NET 7. Gains are usually small per call but free.

3. Effect on testing and mocking

  • Mocking libraries like Moq and NSubstitute create subclasses at run time, so they can't mock sealed classes (or non-virtual members).
  • That's fine when you follow good practice: mock interfaces at boundaries (repositories, gateways, clocks), and use real objects for domain classes and services' internal collaborators.
  • If you find yourself wanting to mock a sealed concrete class, it's usually a sign that a boundary is missing an interface or the test is too implementation-focused.
  • For framework types you don't own (e.g. HttpClient), mock at a different seam (HttpMessageHandler, IHttpClientFactory).

4. Versioning rules

Change Breaking?
Unsealing a sealed class No
Sealing an unsealed public class Yes — breaks anyone who derived from it
Adding sealed to an override Yes for subclasses that override it further
Removing sealed from an override No

For public library types, sealing is a stronger commitment: you can unseal later without breaking anyone, but you can't seal once published without a major version. For application code where you own all callers, sealing is pure benefit — you can unseal any time you actually need inheritance.

5. When not to seal

  • Types designed as extension points (ControllerBase, BackgroundService, DelegatingHandler, your own abstract templates).
  • Types where frameworks need to create proxies (EF Core lazy-loading proxies and change-tracking proxies require non-sealed entities with virtual navigations; Castle DynamicProxy-based interceptors also need non-sealed types).
  • Base classes in a deliberate hierarchy.

6. A production example

We enabled CA1852 as a warning across a large solution, sealed ~600 internal classes in one pull request (mostly mechanical with dotnet format), and saw a small but measurable throughput improvement in a CPU-heavy pricing service (a few percent, from devirtualised Equals/GetHashCode calls in dictionary-heavy code). The bigger win was design clarity: a couple of builds failed because tests were subclassing services to override methods — which exposed missing interfaces at real boundaries that we then fixed.

🔍 What the interviewer is checking

Checks whether the candidate sees sealed as a design and API decision, with a performance bonus.

Green flags: class versus member sealing; devirtualisation and cheaper type checks; CA1852; testing via interfaces instead of mocking classes; knows sealing is breaking and unsealing is not; mentions EF proxies or other cases where sealing hurts.

Red flags: "sealed is for security" without explanation; never seals anything; unseals to mock; thinks sealing makes types immutable.

📚 Learn in depth

1. What the JIT does with sealed types

  • Virtual call on a sealed receiver: the JIT knows the exact method, emits call (or inlines it).
  • Interface call on a sealed receiver: the JIT can resolve the implementing method directly.
  • is/as/casts: for a sealed target type, obj is Foo compiles to obj != null && obj->MethodTable == Foo_MT — no call to a casting helper.
  • GetType() == typeof(Foo) patterns similarly simplify.
bool IsOrder(object o) => o is Order;   // Order sealed -> single pointer compare

With dynamic PGO, unsealed types can get guarded devirtualisation, but sealed types get it unconditionally and without the guard.

2. Covariance checks

Storing into an array of reference type requires a runtime type check (array covariance). If the array's element type is sealed, the JIT knows the exact element type and can often skip the expensive path.

3. Sealed records

For records, sealed removes the need for virtual EqualityContract checks to be overridable, simplifies generated Equals/PrintMembers, and the compiler can make ToString override sealed (C# 10 allows sealed override ToString in records so derived records don't regenerate it).

4. CA1852 and tooling

  • CA1852 — Seal internal types: flags internal types without derived types in the same assembly (safe because no external code can derive).
  • For public types, sealing is a design decision — there's no analyser that can know external usage.
  • dotnet format analyzers --diagnostics CA1852 can apply fixes in bulk.
  • Templates: some teams configure .editorconfig code-style rules and class templates so new classes are created sealed.

5. Testing without subclassing

Need Approach
Replace an external dependency Interface at the boundary + fake
Control time TimeProvider / FakeTimeProvider
HTTP calls Custom HttpMessageHandler or a library like WireMock.Net
Database Testcontainers with real SQL Server, or EF Core in-memory SQLite for some cases
Verify interactions inside a class Usually a smell; test outcomes instead

6. EF Core proxies

Lazy-loading proxies (UseLazyLoadingProxies) and change-tracking proxies generate subclasses at run time; they require public, non-sealed entity types and virtual navigation properties. If you don't use proxies (generally recommended — explicit loading and projections), entities can be sealed. EF Core's default snapshot change tracking works fine with sealed classes.

Try it yourself

  1. Benchmark o is Order with Order sealed and unsealed, and a virtual method call on each.
  2. Enable CA1852 on a project and count the internal types it flags.
  3. Try to mock a sealed class with Moq and read the error; then introduce an interface at the boundary instead.
  4. Unseal a class and confirm nothing breaks; seal a public class in a library and see what breaks for consumers.

💻 Code

```csharp // Application code: sealed unless designed for extension public sealed class OrderTotalCalculator(ITaxRates rates) { public Money Total(Order order) => order.Lines.Sum(l => l.Price * l.Quantity) * (1 + rates.For(order.Region)); } // sealed override locks one step in a middle layer public abstract class Notification { public abstract string Channel { get; } public virtual string Format(string body) => body; } public class EmailNotification : Notification { public override string Channel => "email"; public sealed override string Format(string body) => $"

{WebUtility.HtmlEncode(body)}

"; } public sealed class MarketingEmail : EmailNotification { // public override string Format(string body) => body; // CS0239: Format is sealed } // Testing a sealed class: fake the boundary, not the class public interface ITaxRates { decimal For(string region); } sealed class FakeRates : ITaxRates { public decimal For(string region) => 0.10m; } [Fact] public void Total_adds_tax() { var calc = new OrderTotalCalculator(new FakeRates()); Assert.Equal(110m, calc.Total(OrderWithLine(100m)).Amount); } ```

⚠ Common mistakes

1. Leaving classes unsealed by habit

What happens: every class is open for inheritance by default. Why it is wrong: unintended subclassing, fragile designs, missed optimisations. Do instead: seal by default; unseal deliberately.

2. Unsealing to mock

What happens: classes are unsealed and methods made virtual so tests can mock them. Why it is wrong: design bent for test tooling; tests coupled to implementation details. Do instead: mock interfaces at boundaries and use real objects internally.

3. Sealing public library types later

What happens: a library seals a class in a minor version. Why it is wrong: breaks consumers who derived from it. Do instead: decide sealing before the first release; seal only in major versions.

4. Sealing entities used with proxies

What happens: EF Core lazy-loading proxies enabled with sealed entities. Why it is wrong: proxies can't be generated; runtime errors. Do instead: avoid lazy-loading proxies, or keep those entities unsealed with virtual navigations.

5. Expecting big performance gains everywhere

What happens: sealing presented as a major optimisation for I/O-bound services. Why it is wrong: gains are small per call; they matter in hot, CPU-bound paths. Do instead: seal for design reasons; treat performance as a bonus and measure hot paths.

6. Sealing designed extension points

What happens: base classes meant for extension are sealed. Why it is wrong: blocks legitimate use. Do instead: keep deliberate extension points open and document them.

7. Forgetting sealed override

What happens: an override that shouldn't be changed further remains virtual. Why it is wrong: deeper subclasses can alter behaviour unexpectedly. Do instead: use sealed override where the chain should end.

8. Using inheritance to "extend" sealed framework types

What happens: frustration that string or HttpClient can't be subclassed. Why it is wrong: those types are deliberately closed. Do instead: use composition, extension methods/members, or the provided extension seams.

🏛 Senior insight: trade-offs

Seniors seal by default and open deliberately.

Type Default
DTOs, records, handlers, services sealed
Domain entities (no lazy-loading proxies) sealed
Framework extension points Unsealed, with documented virtual members
Public library types Decide before 1.0; unsealing later is safe, sealing is not
Middle layer of a template-method hierarchy Unsealed, with sealed override on fixed steps

They link sealing to the open/closed principle: a class should be open for extension through its designed seams (interfaces, strategies, events), not through arbitrary subclassing.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says sealed helps performance Why: Checks how virtual dispatch and devirtualisation work
    How to answer

    A virtual member can be overridden by a derived class, and a call to it is resolved by the run-time type of the object, not the variable's type. In C#, members are non-virtual by default (unlike Java), because every virtual member is a promise to subclasses and is harder to change. At run time the call loads the object's method table, reads the method's slot and calls through it, which prevents inlining unless the JIT can devirtualise (exact type known, sealed class or method, or dynamic PGO's guarded devirtualisation). Methods, properties, indexers and events can be virtual; static (outside interfaces), private and constructors cannot.

  2. When: Candidate says sealing prevents misuse of inheritance Why: Probes fragile base class and LSP
    How to answer

    Inheritance lets a derived class reuse and specialise a base class: class SavingsAccount : Account. C# allows one base class but many interfaces. The derived class inherits all members except constructors and finalisers (private ones exist but are not accessible), can call base members with base., and can override virtual/abstract members. Declaring a member with the same name without override hides it (new), and which one runs then depends on the variable's static type, a common source of bugs. Use inheritance only for a real is-a relationship where a derived object can be used anywhere the base is expected (Liskov substitution). Deep hierarchies, inheriting just to reuse code, and fragile base classes are the main problems; composition is often better.

🛠 Real-world problems: how you handle them

Sealing an API codebase and replacing class mocks with boundary fakes
Situation
A [120]-project .NET solution had almost no sealed classes. Tests mocked concrete services with Moq by making methods virtual.
Signals
Methods were made virtual only for tests, and one production subclass had overridden a virtual `Validate` method and skipped a required check.
Investigation
Searched for subclasses outside tests and found only [four] legitimate inheritance hierarchies. Measured serialisation and handler dispatch with BenchmarkDotNet.
Resolution
Enabled CA1852 as a warning, sealed all non-hierarchy classes, removed test-only `virtual` modifiers and introduced interfaces at I/O boundaries for the tests that needed fakes.
Prevention
CA1852 stays at warning level with `TreatWarningsAsErrors` in CI; review rule: a class is unsealed only with a comment explaining its extension design.

How to tell it (STAR)

**Situation:** in a large solution, methods were virtual only so tests could mock them, and one subclass skipped validation. **Task:** make the design intent explicit without breaking tests. **Action:** I enabled CA1852, sealed every class not in a real hierarchy, removed test-only virtuals and replaced class mocks with fakes behind small interfaces at I/O boundaries. **Result:** the validation bypass could no longer happen, serialisation benchmarks improved by about [5%], and [hundreds of] lines of brittle mock setup were removed.

↩ Usually asked after

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me