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

📚 Interview question

What is an abstract class in C#, and when would you use one? Explain abstract members, constructors and state in an abstract class, the template method pattern, and how abstract classes evolve compared with interfaces.

Abstract classes in C#: abstract versus virtual members, constructors and shared state, the template method pattern, BCL examples and versioning trade-offs.

Mid-Level Theory Intermediate abstract-class csharp inheritance oop template-method

🎯 Key points

  • An abstract class cannot be instantiated and may declare abstract members with no body
  • Every non-abstract derived class must override all abstract members (CS0534)
  • Abstract classes can hold instance state, constructors and implemented members, unlike interfaces
  • Constructors should be protected; they enforce shared invariants for every subtype
  • Template method: a non-virtual public method fixes the steps and calls protected abstract or virtual hooks
  • A class can derive from only one base class, so an abstract base uses the single inheritance slot
  • Adding a virtual member with a body is non-breaking; adding an abstract member breaks every subclass
  • BCL examples: `Stream`, `DbConnection`, `HttpMessageHandler`, `BackgroundService`

⏱ 30-second answer

An abstract class is a base class that cannot be instantiated and may declare abstract members with no body, which every non-abstract derived class must override. Unlike an interface it can hold instance fields, constructors and fully implemented members, so it is the place for shared state and a shared algorithm. The classic use is the template method pattern: a public non-virtual method defines the steps and calls protected abstract or protected virtual hooks. Constructors should be protected. A class can derive from only one abstract class. Adding a new virtual member with a body is non-breaking, but adding a new abstract member breaks every derived class.

🗣 Full answer (senior)

How I would answer this in an interview

An abstract class is a class that can't be instantiated and may contain abstract members (no body) that derived classes must implement, alongside ordinary members with implementation and state. It's for a family of closely related types that share both a contract and real behaviour. I'd cover abstract members, constructors and state, the template method pattern, and how abstract classes evolve differently from interfaces.

1. Abstract members

public abstract class PaymentProcessor(ILogger logger, TimeProvider clock)
{
    protected ILogger Logger { get; } = logger;

    // Template method: fixed algorithm, not overridable
    public async Task<PaymentResult> ProcessAsync(PaymentRequest request, CancellationToken ct)
    {
        Validate(request);
        var started = clock.GetTimestamp();
        var result = await ChargeAsync(request, ct);            // varies per provider
        Logger.LogInformation("Charged {Amount} via {Provider} in {Elapsed} ms",
            request.Amount, ProviderName, clock.GetElapsedTime(started).TotalMilliseconds);
        await OnChargedAsync(result, ct);                       // optional hook
        return result;
    }

    protected abstract string ProviderName { get; }
    protected abstract Task<PaymentResult> ChargeAsync(PaymentRequest request, CancellationToken ct);
    protected virtual Task OnChargedAsync(PaymentResult result, CancellationToken ct) => Task.CompletedTask;

    protected virtual void Validate(PaymentRequest request) =>
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(request.Amount.Value);
}
  • abstract members have no body and must be overridden by every concrete derived class (they're implicitly virtual).
  • virtual members have a default and may be overridden ("hooks").
  • Non-virtual members are fixed behaviour that subclasses can't change — like ProcessAsync above.
  • A class with any abstract member must be declared abstract.

2. Constructors and state

  • Abstract classes can have constructors — usually protected — called by derived classes via base(...). They initialise shared state and enforce invariants for the whole family.
  • They can hold fields, including private state that subclasses can't touch.
  • They can implement interfaces, providing some members and leaving others abstract.

This is the key capability interfaces lack: interfaces can't have instance state or constructors.

3. The template method pattern

The base class defines the skeleton of an algorithm (validation → charge → log → hook) and lets subclasses fill in steps. Benefits: cross-cutting steps (logging, timing, validation, error handling) can't be forgotten by subclasses, and the order is fixed in one place. .NET examples:

  • BackgroundService.ExecuteAsync — the base handles StartAsync/StopAsync and cancellation.
  • Stream — many concrete methods built on a few abstract ones (Read, Write, Seek).
  • DelegatingHandler.SendAsync, JsonConverter<T>.Read/Write, AuthenticationHandler<TOptions>.HandleAuthenticateAsync.

4. When I use one

  • A real is-a family with shared state and behaviour (payment processors, report generators, framework extension points).
  • I want to control the flow (template method) and keep extension points narrow.
  • I'm providing a convenience base for an interface: IHostedService + BackgroundService, IComparer<T> + Comparer<T>.

I avoid it for unrelated types needing a common capability (use an interface), for structs (can't inherit), and when variation is better composed (strategies).

5. Evolving abstract classes vs interfaces

Change Abstract class Interface
Add a member with a default body Non-breaking (new virtual method) Non-breaking with a default interface method (C# 8+)
Add an abstract member Breaking for derived classes Breaking without a default
Add state/fields Easy (private fields) Impossible
Multiple inheritance No Yes
Works with structs No Yes

Historically abstract classes were the safer choice for evolving public APIs ("prefer classes over interfaces" in the Framework Design Guidelines). Default interface methods narrowed the gap, but abstract classes still win when evolution needs state or protected helpers.

6. A production example

Four payment provider integrations each implemented IPaymentGateway from scratch; two forgot idempotency-key handling and one didn't log latency. We introduced an abstract PaymentGatewayBase implementing the interface, with a sealed template ChargeAsync handling idempotency, retries, metrics and logging, and an abstract SendChargeAsync for the provider call. New providers became ~50 lines, and the cross-cutting behaviour became impossible to forget. We kept the interface so the application layer and tests didn't depend on the base class.

🔍 What the interviewer is checking

Checks that the candidate can explain abstract classes in terms of design, not only syntax.

Green flags: abstract versus virtual members; constructors and state; protected constructors; template method with a non-virtual public method; BCL examples such as Stream or BackgroundService; versioning (abstract members break subclasses); knows when to move to composition.

Red flags: "an abstract class is an interface with some code"; cannot say why you would pick it over an interface; virtual template methods; deep hierarchies used for reuse.

📚 Learn in depth

1. What abstract means to the compiler and runtime

  • The C# compiler forbids new AbstractType() (CS0144). The type is marked abstract in metadata, and the runtime also refuses to instantiate it.
  • Abstract methods are emitted as virtual abstract with no IL body; they get vtable slots that derived classes fill. A concrete class with an unfilled slot can't load (TypeLoadException), which the compiler prevents.
  • Abstract properties are abstract getter/setter methods.
  • A derived class can be abstract itself and leave members abstract for its subclasses, or re-abstract a virtual member (public abstract override string ToString();) to force further subclasses to implement it.

2. Protected constructors

A public constructor on an abstract class is pointless (nobody can call it directly) and analysers (CA1012) suggest protected. The constructor runs as part of the derived constructor chain, so it can validate shared parameters and establish invariants for every subclass.

Avoid calling abstract/virtual members from the base constructor (CA2214): the derived constructor body hasn't run yet.

3. Sealing the template

To stop subclasses overriding the algorithm:

  • Make the template method non-virtual (the default in C#), or
  • If it overrides an inherited virtual method, mark it sealed override.

Keep hooks protected (not public) so callers use the template, not the steps. Name hooks clearly (OnChargedAsync, ChargeCoreAsync) — the BCL uses the ...Core suffix convention (DisposeAsyncCore, ReadCore).

4. Abstract classes and testing

  • You can't mock an abstract class's non-virtual members with most mocking libraries (Moq and NSubstitute mock virtual/abstract members only).
  • To test the template logic, create a small test subclass in the test project with predictable step implementations.
  • To test a concrete subclass, test it through the public template method.
  • Keeping an interface alongside the base class lets consumers mock the interface without caring about the base class.

5. Abstract classes vs composition for the same problem

The template method can often be replaced by composition: PaymentProcessor taking an IProviderClient strategy. Trade-offs:

Template method (inheritance) Strategy (composition)
Steps share base state and helpers easily Steps get data via parameters
One axis of variation Multiple independent axes
Fixed at compile time Swappable at run time
Subclass coupled to base internals Coupled only to the strategy interface

For one stable axis of variation with lots of shared behaviour, the template method is simple and clear. For multiple or changing axes, compose.

6. Abstract classes in EF Core

Abstract entity base classes (e.g. AuditableEntity with CreatedAt, UpdatedBy) are common. If the base isn't mapped as an entity, EF Core just maps inherited properties into each derived table. If you map the hierarchy (TPH/TPT/TPC), the abstract base becomes part of the inheritance mapping — decide deliberately; shared audit columns usually don't need a mapped hierarchy.

Try it yourself

  1. Implement a template method with abstract and virtual hooks and a test subclass that records the call order.
  2. Re-abstract ToString() in a base class and see the compiler force subclasses to implement it.
  3. Refactor the same logic into a strategy-based design and compare readability and tests.
  4. Enable CA1012 and CA2214 and check your abstract classes.

💻 Code

public abstract class Exporter
{
    private readonly IAuditLog _audit;

    protected Exporter(IAuditLog audit) =>
        _audit = audit ?? throw new ArgumentNullException(nameof(audit));

    // Template method: not virtual, so no subclass can skip a step
    public async Task<ExportFile> ExportAsync(User user, Query q, CancellationToken ct)
    {
        if (!user.CanExport) throw new UnauthorizedAccessException();
        var rows = await q.RunAsync(ct);
        var bytes = Render(rows);
        await _audit.RecordAsync(user.Id, GetType().Name, rows.Count, ct);
        return new ExportFile(FileName(q), ContentType, bytes);
    }

    protected abstract string ContentType { get; }
    protected abstract byte[] Render(IReadOnlyList<Row> rows);
    protected virtual string FileName(Query q) => $"{q.Name}-{DateTime.UtcNow:yyyyMMdd}";
}

public sealed class CsvExporter(IAuditLog audit) : Exporter(audit)
{
    protected override string ContentType => "text/csv";
    protected override byte[] Render(IReadOnlyList<Row> rows) =>
        Encoding.UTF8.GetBytes(string.Join('\n', rows.Select(r => r.ToCsv())));
}

// BCL example: BackgroundService is abstract
public sealed class OutboxWorker(IOutbox outbox) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
            await outbox.DispatchPendingAsync(stoppingToken);
    }
}

⚠ Common mistakes

1. Abstract classes for unrelated types

What happens: a BaseEntityService or BaseHelper used just to share utility methods. Why it is wrong: creates false is-a relationships and couples unrelated classes. Do instead: use composition or static helpers; reserve abstract classes for real families.

2. Public template steps

What happens: hook methods are public, so callers invoke steps directly and skip validation/logging. Why it is wrong: breaks the guarantees the template provides. Do instead: make hooks protected and the template the only public entry point.

3. Overridable templates

What happens: the template method is virtual, and subclasses override it entirely. Why it is wrong: subclasses bypass cross-cutting behaviour. Do instead: keep the template non-virtual (or sealed override).

4. Calling abstract members from the base constructor

What happens: the base constructor calls ProviderName or another abstract member. Why it is wrong: derived fields aren't initialised yet — NREs and wrong values. Do instead: call abstract members lazily or in an explicit initialisation step.

5. Adding abstract members to a published base class

What happens: a library adds a new abstract method in a minor version. Why it is wrong: every derived class breaks. Do instead: add virtual members with sensible defaults.

6. Too many virtual hooks

What happens: every step is virtual "for flexibility". Why it is wrong: subclasses can change behaviour in unpredictable combinations; the base contract becomes unclear. Do instead: expose a few deliberate hooks; keep the rest private.

7. Abstract base without an interface in application code

What happens: consumers depend on the abstract class directly. Why it is wrong: tests must subclass or mock non-virtual members; consumers are coupled to implementation. Do instead: keep an interface for consumers and use the abstract class as an implementation helper.

8. Public constructors on abstract classes

What happens: public PaymentProcessor(...). Why it is wrong: misleading; the class can't be instantiated directly (CA1012). Do instead: make constructors protected.

🏛 Senior insight: trade-offs

Seniors use an abstract class when there is real shared state or a fixed algorithm, and switch to composition when variation has more than one axis.

Need Choose
Fixed workflow, steps vary by type Abstract class with a template method
Shared invariants on constructor arguments Abstract class with a protected constructor
A framework extension point that will grow Abstract class (add virtual members later)
Many unrelated types share a capability Interface
Several independent axes of variation Composition with strategies
Consumers should not know about the base Interface for consumers plus abstract base for implementers

They also say why the BCL made Stream an abstract class: dozens of members were added over the years (ReadAsync, CopyToAsync, Read(Span<byte>)) as virtuals with default bodies, and no existing stream broke.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate compares with interfaces Why: Checks modern interface features
    How to answer

    An interface is a contract: a set of members a type promises to provide, with no instance state. A class or struct can implement many interfaces. Since C# 8 an interface can carry default implementations (used only through the interface, and they need .NET Core 3.0+), plus static, private and protected members. Since C# 11 it can declare static abstract / static virtual members, which power generic math (INumber<T>, T.Zero). Explicit implementation (void IDisposable.Dispose()) hides a member unless you cast. Calls through an interface use interface dispatch, which the JIT can often devirtualise. Casting a struct to an interface boxes it; a generic constraint (where T : IComparable<T>) avoids the box.

  2. When: Candidate mentions protected constructors Why: Probes construction order and virtual calls in constructors
    How to answer

    A constructor runs when an object is created and should leave it valid. If you declare none, the compiler adds a public parameterless one. Constructors can chain: : this(...) calls another constructor of the same class, : base(...) calls the base class's. For new Derived(), the order is: derived field initialisers, base field initialisers, base constructor body, derived constructor body. A static constructor runs once, before the type is first used, and is thread-safe. Primary constructors (C# 12) put parameters on the class declaration; they are captured as hidden fields, not properties, and are mutable. required members (C# 11) force callers to set them in an object initialiser. Calling a virtual method from a constructor runs the derived override before the derived constructor body has run, so it can see uninitialised state.

  3. When: Candidate mentions a second axis of variation Why: Checks they know when to switch to composition
    How to answer

    Composition means a class has other objects as parts and delegates work to them, instead of inheriting behaviour. A Car has an Engine and Wheels; an OrderProcessor has a validator, a pricer and a notifier. In the strict UML sense, composition also means ownership: the parts are created and destroyed with the whole and are not shared. Benefits: behaviour can be swapped (pass a different IPricingStrategy), combined freely (decorators wrap each other), tested in isolation, and changed without a fragile base class. The cost is a little more wiring, usually handled by constructors or dependency injection.

🛠 Real-world problems: how you handle them

Missing audit records fixed by moving export flow into an abstract base
Situation
A reporting module had [five] export formats, each a separate class with its own copy of load, permission check, audit and render code.
Signals
A compliance review found that PDF and Excel exports wrote no audit records. Users with revoked export permission could still export Excel files.
Investigation
I diffed the five classes. Each had drifted: two missed the audit call and one checked permission after running the query.
Resolution
Created an abstract `Exporter` with a protected constructor taking `IAuditLog`, a non-virtual `ExportAsync` that does the checks and audit in a fixed order, and abstract `Render`/`ContentType`. Each format became a small sealed subclass.
Prevention
Added a test that runs every `Exporter` subclass (found by reflection) and asserts that an audit record is written. Code review rule: export logic lives only in the base.

How to tell it (STAR)

**Situation:** compliance found that [two of five] export formats wrote no audit record. **Task:** fix the gap and stop it coming back. **Action:** I moved the shared flow into an abstract `Exporter` with a non-virtual template method, so subclasses only render. I added a reflection-based test that checks every exporter writes an audit entry. **Result:** the audit gap closed, the export code shrank by about [60%], and a sixth format later took [half a day] instead of copying a whole flow.

↩ Usually asked after

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me