Abstract Classes
An abstract class can’t be instantiated and can declare members subclasses must implement. Use abstract classes for partial implementations with template-method patterns; reach for interfaces when you only need contracts without shared behaviour.
abstract, virtual, sealed, template
EXAMPLE
// 1) abstract class — partial implementation + required hooks
public abstract class Repository<T>
{
protected readonly DbContext Db;
protected Repository(DbContext db) => Db = db;
public T? GetById(int id) => Db.Set<T>().Find(id);
public IEnumerable<T> List() => ApplyDefaultFilters(Db.Set<T>()).ToList();
protected abstract IQueryable<T> ApplyDefaultFilters(IQueryable<T> query);
// ^ subclasses MUST implement
}
public class ActiveUserRepository : Repository<User>
{
public ActiveUserRepository(DbContext db) : base(db) { }
protected override IQueryable<User> ApplyDefaultFilters(IQueryable<User> q) =>
q.Where(u => u.IsActive);
}
// You can't 'new Repository<User>()' — the compiler refuses.
// Subclasses fill in the holes; shared logic lives in the base.
// 2) abstract members — methods AND properties
public abstract class Shape
{
public abstract double Area { get; } // abstract property
public abstract void Describe(); // abstract method
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double r) { Radius = r; }
public override double Area => Math.PI * Radius * Radius;
public override void Describe() => Console.WriteLine($"Circle r={Radius} A={Area:F2}");
}
// 3) abstract + virtual — mix required and optional overrides
public abstract class Animal
{
public abstract string Sound(); // must override
public virtual string Movement() => "walks"; // can override
public string Describe() => $"A {GetType().Name} that {Movement()} and says {Sound()}";
}
public class Dog : Animal
{
public override string Sound() => "woof";
// Movement not overridden — uses default 'walks'
}
public class Fish : Animal
{
public override string Sound() => "blub";
public override string Movement() => "swims";
}
// 4) Template method pattern — abstract steps + concrete algorithm
public abstract class ReportBuilder
{
public string Build()
{
var header = LoadHeader();
var body = LoadBody();
var footer = LoadFooter();
return Format(header, body, footer);
}
protected abstract string LoadBody(); // subclasses customise the body
protected virtual string LoadHeader() => "Report";
protected virtual string LoadFooter() => "-- end --";
protected virtual string Format(string h, string b, string f) => $"{h}\n{b}\n{f}";
}
public class WeeklyReport : ReportBuilder
{
protected override string LoadBody() => "weekly stats here";
}
// 5) abstract class vs interface — when to use which
// abstract class wins when:
// • Sharing concrete fields, methods, or constructors
// • Common base behaviour with extension points (template method)
// • Need protected members (interfaces are all public)
//
// interface wins when:
// • Pure contract; many unrelated types implement it
// • Multiple inheritance (a class can implement many interfaces, extend one class)
// • Mix-in of behaviour via default interface methods (C# 8+)
//
// Modern guidance: prefer interfaces + composition; reach for abstract class for genuine 'is-a' families with shared state.
// 6) sealed class + abstract base — closed family
public abstract record Event
{
public DateTimeOffset OccurredAt { get; init; } = DateTimeOffset.UtcNow;
}
public sealed record OrderPlaced(string OrderId, long TotalCents) : Event;
public sealed record OrderCancelled(string OrderId, string Reason) : Event;
public sealed record OrderShipped(string OrderId, string Carrier, string TrackingNumber) : Event;
static string Describe(Event e) => e switch
{
OrderPlaced p => $"placed {p.OrderId} (${p.TotalCents/100m:F2})",
OrderCancelled c => $"cancelled {c.OrderId}: {c.Reason}",
OrderShipped s => $"shipped {s.OrderId} via {s.Carrier} #{s.TrackingNumber}",
};
// Compiler-verified exhaustiveness when Event is abstract + every concrete subtype is sealed.
// 7) Constructors — abstract classes CAN have them
public abstract class HttpClientBase
{
private readonly HttpClient _client;
protected HttpClientBase(HttpClient client) => _client = client;
protected Task<HttpResponseMessage> GetAsync(string url) => _client.GetAsync(url);
}
public class GitHubApi : HttpClientBase
{
public GitHubApi(HttpClient client) : base(client) { }
public Task<HttpResponseMessage> Repos() => GetAsync("https://api.github.com/user/repos");
}
// 8) sealed override — prevent further override
public class Specific : Repository<User>
{
public Specific(DbContext db) : base(db) { }
protected sealed override IQueryable<User> ApplyDefaultFilters(IQueryable<User> q) => q;
// ^ no subclass of Specific can re-override this method
}
// 9) abstract class with extension points + DI
public abstract class JobBase
{
private readonly ILogger _log;
protected JobBase(ILogger log) => _log = log;
public async Task RunAsync(CancellationToken ct)
{
_log.LogInformation("start");
try { await ExecuteAsync(ct); }
catch (Exception e) { _log.LogError(e, "job failed"); throw; }
_log.LogInformation("done");
}
protected abstract Task ExecuteAsync(CancellationToken ct);
}
public class CleanupJob : JobBase
{
public CleanupJob(ILogger<CleanupJob> log) : base(log) { }
protected override Task ExecuteAsync(CancellationToken ct) => /* cleanup */ Task.CompletedTask;
}
// 10) Common bugs
// • Forgetting 'override' on a method intended to override — compiler errors out (good)
// • Calling an abstract method from the base constructor — runs on a half-built object
// • Adding new abstract members later breaks every subclass — provide a default or use virtual
// • abstract class with too many concrete helpers — extract to a service class
// • abstract class that has no abstract members — just use a non-abstract base + sealed leaves
// • Mixing abstract base + interface with same method — disambiguate explicitly
// • Allowing public state on the abstract base — leaks invariants to subclasses; keep state protected with controlled mutation
Why it matters
Reach for abstract classes when you have shared concrete behaviour plus required extension points (template-method pattern). Pair with sealed record subtypes for pattern-matching exhaustiveness, keep state protected with controlled mutation, and don’t call virtual or abstract methods from a constructor — subclass code runs before its fields are initialised.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
public abstract class Shape {
public abstract double Area();
}
public class Circle : Shape {
public double R { get; }
public Circle(double r) { R = r; }
public override double Area() => Math.PI * R * R;
}
Try it Yourself »
Discussion
Loading…