Back to Blog
C#

C# Abstract Class Inheritance Explained

c# abstract class inheritance: Learn how C# abstract class inheritance works: abstract members, constructor flow, sealed overrides, and when an abstract class beats an...

abstract classinheritanceC# OOPinterfacespolymorphism
Diagram showing an abstract base class with derived classes inheriting and overriding abstract members in C#

C# abstract class inheritance is the mechanism that lets a base class define a contract and shared implementation while forcing derived classes to complete the missing pieces. An abstract class cannot be instantiated directly. It exists to describe what a family of types has in common, and it relies on derived classes to supply the behavior that varies.

What an Abstract Class Actually Provides

When you write public abstract class PaymentProcessor, you are telling the compiler: this class is incomplete by design, and only a derived class that fills in the missing pieces can be created with new. The core of abstract class inheritance is the abstract member. An abstract method or property has no implementation in the base class:

public abstract class PaymentProcessor { public abstract PaymentResult Process(PaymentRequest request); }

Any non-abstract derived class must override Process. The compiler enforces this, so you cannot accidentally forget an implementation and still compile. That enforcement is the main practical difference between an abstract member and a virtual member. A virtual member provides a default implementation that derived classes may override; an abstract member provides nothing and forces the override.

Declaring Abstract Members Correctly

Abstract members can be methods, properties, events, and indexers. They cannot be fields, and they cannot have a body. The syntax for an abstract property is similar to an interface property:

public abstract class Report { public abstract string Title { get; } public abstract DateTime GeneratedAt { get; } public abstract void Export(Stream destination); }

A derived class implements these members with the override keyword:

public class CsvReport : Report { public override string Title => "Monthly CSV Report"; public override DateTime GeneratedAt { get; } = DateTime.UtcNow; public override void Export(Stream destination) { // Write CSV rows to the destination stream. } }

An abstract property with only a getter can be implemented with an auto-property. The compiler treats the override as satisfying the abstract contract as long as the accessors match. Abstract members cannot be private, sealed, or static. They must be accessible to derived classes, so protected and public are the common choices. If you mark an abstract member internal, derived classes in other assemblies cannot override it, which effectively makes the member unusable outside the assembly.

Constructor Flow in an Abstract Class Hierarchy

Constructors in an abstract class behave like constructors in any base class, with one important detail: you cannot call new on the abstract class, but its constructor still runs when a derived instance is created. The base constructor executes before the derived constructor body.

public abstract class DatabaseConnection { protected DatabaseConnection(string connectionString) { ConnectionString = connectionString; } protected string ConnectionString { get; } } public class SqlServerConnection : DatabaseConnection { public SqlServerConnection(string connectionString) : base(connectionString) { } }

If the abstract class has no explicit constructor, the compiler adds a parameterless protected constructor. If it has a constructor with parameters, every derived class must call it, either explicitly with base(...) or implicitly when a parameterless base constructor exists. A common mistake is to assume abstract classes cannot have state. They can hold fields and properties just like normal classes. The protected access modifier is the key tool here: it lets derived classes read and modify shared state while keeping it hidden from external callers.

Abstract Classes vs Interfaces

The decision between an abstract class and an interface is one of the most frequent design questions in C#. The rules have changed over time, so it is worth being precise about the current behavior. An interface defines a contract with no implementation. A class can implement multiple interfaces. An abstract class can provide implementation, state, and constructor logic, but a class can inherit from only one abstract class.

CapabilityAbstract classInterface
Instance fieldsYesNo
Constructor logicYesNo
Method implementationYesYes (default interface methods)
Multiple inheritanceNoYes
Access modifiers on membersYesPublic by default
Sealed membersYesNo

Use an abstract class when the derived types share significant implementation or state. Use an interface when you need to model a capability that unrelated types can implement. A Stream is a good example of an abstract class: every stream shares positioning, reading, and writing logic, but the underlying storage differs. An IDisposable is a good example of an interface: file handles, database connections, and timers all need disposal, but they share no implementation.

Sealing an Override to Stop Further Inheritance

Sometimes you want a derived class to implement an abstract member but prevent further overrides in deeper subclasses. The sealed modifier on an override accomplishes this:

public abstract class Shape { public abstract double Area { get; } } public class Circle : Shape { public Circle(double radius) => Radius = radius; public double Radius { get; } public sealed override double Area => Math.PI * Radius * Radius; } public class FancyCircle : Circle { // Cannot override Area here. The compiler rejects it. }

This is useful when the implementation is fundamental to the type's invariants. For example, if Area is computed from Radius, allowing a subclass to override Area could produce inconsistent geometry. Sealing the override keeps the derived class hierarchy predictable. Sealing an override does not prevent the class itself from being inherited. It only prevents that specific member from being overridden further down.

Where Abstract Class Inheritance Commonly Breaks

A few patterns cause real problems in production code. The first is calling virtual or abstract members from a base constructor. When the base constructor runs, the derived constructor body has not executed yet, so the derived object is not fully initialized:

public abstract class Logger { protected Logger() { Write("Logger initialized"); // Calls the derived override before derived fields are set. } public abstract void Write(string message); } public class FileLogger : Logger { private readonly string _path = "/var/log/app.log"; public override void Write(string message) { File.AppendAllText(_path, message); } }

In this example, _path is assigned after the base constructor runs, so Write receives a null _path when the base constructor invokes it. The fix is to avoid calling virtual or abstract members from constructors entirely, or to use a two-phase initialization pattern where the derived class explicitly calls an initialization method.

A second common failure is overusing abstract classes for shallow hierarchies. If a base class has one derived class and no shared logic, the abstraction adds indirection without value. A concrete class with virtual members is often simpler. A third issue is breaking the single-responsibility boundary. Abstract classes that accumulate many abstract members force every derived class to implement features it does not need. If a derived class must throw NotSupportedException for a member, that member probably belongs in a separate interface.

Maintainability: Favoring Composition Where It Fits

Abstract class inheritance creates a strong coupling between the base and derived types. Changing an abstract member signature breaks every derived class at compile time, which is usually good because the compiler catches the problem. But changing the base class's behavior can have subtle effects on all derived classes, especially when protected state is shared.

When the relationship between types is genuinely an "is-a" relationship and the base provides real shared behavior, abstract class inheritance is the right tool. When the relationship is "can-do" or "has-a", prefer interfaces and composition. A repository that delegates to a StreamWriter is easier to test and evolve than a repository that inherits from a base repository class with file-writing logic baked in.

The practical rule: start with an interface when you only need a contract, add an abstract class when you have shared implementation that multiple derived classes genuinely reuse, and avoid deep hierarchies beyond two or three levels. Deep hierarchies make behavior hard to trace and make small changes risky across many derived types.

c# abstract class inheritance: Practical Usage and Code Exam | RYUSLOG DEV