TL;DR:

Abstract Class in C# - TL;DR

What Is Abstract Class?

  • Definition: Incomplete class that cannot be instantiated directly
  • Purpose: Blueprint for related classes with shared behavior
  • Keywords: abstract class, abstract methods, override in derived classes

Key Features

  • Mixed Implementation: Can have both concrete methods and abstract methods
  • State: Can contain fields, properties, constructors
  • Access Modifiers: Supports private, protected, internal members
  • Inheritance: Single inheritance only

Abstract vs Virtual Methods

  • Abstract: No implementation, must be overridden
  • Virtual: Has default implementation, can be overridden
  • Regular: Concrete implementation, cannot be overridden

When to Use Abstract Classes

  • Share code between related classes
  • Need constructors or protected members
  • Template method pattern (define workflow, customize steps)
  • Enforce implementation while providing base functionality

Limitations of Abstract Classes

  • Cannot be instantiated directly
  • Single inheritance only
  • Cannot be sealed

Common Use Cases

  • Framework/SDK base classes
  • Database providers
  • Document processors
  • Any scenario needing shared behavior + enforced implementation

Picture this: you’re building a document management system where users can create Word documents, PDFs, and Excel files. Each document type has its own specific formatting and processing logic, but they all share common behaviors, they need to be saved, validated, and have metadata tracked.

You could create separate classes for each document type, but you’d end up duplicating code. Or you could use a base class, but nothing stops someone from creating an instance of the generic “Document” class, which doesn’t make sense in your domain.

This is where abstract classes shine. They let you define shared behavior while ensuring that only concrete, meaningful implementations can be instantiated.

What Is an Abstract Class in C#?

An abstract class in C# is a class that cannot be instantiated directly but serves as a blueprint for other classes. It’s like a template that defines both concrete methods (with full implementation) and abstract methods (that must be implemented by child classes).

The key insight is that abstract classes represent incomplete concepts. A “Document” by itself is incomplete, it needs to be a specific type of document to be useful. But all documents share certain characteristics and behaviors that you want to define once and reuse.

public abstract class Document
{
    public string Title { get; set; }
    public DateTime CreatedDate { get; protected set; }
    public string Author { get; set; }
    
    protected Document(string title, string author)
    {
        Title = title;
        Author = author;
        CreatedDate = DateTime.Now;
    }
    
    // Concrete method - shared behavior
    public void UpdateMetadata(string newTitle, string newAuthor)
    {
        Title = newTitle;
        Author = newAuthor;
        Console.WriteLine($"Metadata updated for {GetType().Name}");
    }
    
    // Abstract methods - must be implemented by derived classes
    public abstract void Save(string filePath);
    public abstract bool Validate();
    public abstract long GetFileSize();
}

Notice the abstract keyword before the class declaration. This tells the compiler that this class is incomplete and cannot be instantiated directly.

Abstract Methods vs Virtual Methods

Abstract classes can contain both abstract methods and regular methods. Understanding the difference is crucial for effective design.

Abstract methods have no implementation in the base class and must be overridden by derived classes. They’re essentially contracts that say “every subclass must provide this functionality.”

Virtual methods have a default implementation but can be optionally overridden. Here’s how they work together:

public abstract class MediaProcessor
{
    protected string FilePath { get; set; }
    
    // Abstract method - no implementation, must be overridden
    public abstract void Process();
    
    // Virtual method - has default implementation, can be overridden
    public virtual void LogProcessing()
    {
        Console.WriteLine($"Processing {Path.GetFileName(FilePath)}...");
    }
    
    // Regular method - concrete implementation, cannot be overridden
    public void SetFilePath(string path)
    {
        if (string.IsNullOrEmpty(path))
            throw new ArgumentException("File path cannot be empty");
            
        FilePath = path;
    }
}

public class VideoProcessor : MediaProcessor
{
    // Must implement abstract method
    public override void Process()
    {
        // Video-specific processing logic
        Console.WriteLine("Compressing video and generating thumbnails...");
        // Implementation details...
    }
    
    // Can optionally override virtual method
    public override void LogProcessing()
    {
        Console.WriteLine($"[VIDEO] Starting processing of {Path.GetFileName(FilePath)}");
        base.LogProcessing(); // Call parent implementation if needed
    }
}

Real-World Example: Database Connection Framework

Let’s look at a practical example that demonstrates the power of abstract classes in C# inheritance. Imagine you’re building a data access layer that needs to support multiple database providers, SQL Server, MySQL, and PostgreSQL.

public abstract class DatabaseProvider
{
    protected string ConnectionString { get; private set; }
    protected ILogger Logger { get; private set; }
    
    protected DatabaseProvider(string connectionString, ILogger logger)
    {
        ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    // Template method pattern - defines the workflow
    public async Task<T> ExecuteQueryAsync<T>(string query, object parameters = null)
    {
        using var connection = CreateConnection();
        
        try
        {
            await OpenConnectionAsync(connection);
            LogQueryExecution(query);
            
            var result = await ExecuteQueryInternalAsync<T>(connection, query, parameters);
            
            Logger.LogInformation($"Query executed successfully. Rows affected: {GetRowCount(result)}");
            return result;
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, $"Query execution failed: {query}");
            throw;
        }
        finally
        {
            await CloseConnectionAsync(connection);
        }
    }
    
    // Abstract methods - each provider implements these differently
    protected abstract IDbConnection CreateConnection();
    protected abstract Task<T> ExecuteQueryInternalAsync<T>(IDbConnection connection, string query, object parameters);
    protected abstract string BuildConnectionString(DatabaseConfig config);
    
    // Virtual methods - default implementation, can be overridden
    protected virtual async Task OpenConnectionAsync(IDbConnection connection)
    {
        await connection.OpenAsync();
        Logger.LogDebug("Database connection opened");
    }
    
    protected virtual async Task CloseConnectionAsync(IDbConnection connection)
    {
        await connection.CloseAsync();
        Logger.LogDebug("Database connection closed");
    }
    
    // Concrete helper methods
    private void LogQueryExecution(string query)
    {
        var sanitizedQuery = query.Length > 100 ? query.Substring(0, 100) + "..." : query;
        Logger.LogInformation($"Executing query: {sanitizedQuery}");
    }
    
    private int GetRowCount<T>(T result)
    {
        return result is IEnumerable<object> enumerable ? enumerable.Count() : 1;
    }
}

public class SqlServerProvider : DatabaseProvider
{
    public SqlServerProvider(string connectionString, ILogger logger) 
        : base(connectionString, logger) { }
    
    protected override IDbConnection CreateConnection()
    {
        return new SqlConnection(ConnectionString);
    }
    
    protected override async Task<T> ExecuteQueryInternalAsync<T>(IDbConnection connection, string query, object parameters)
    {
        // SQL Server specific implementation using Dapper
        return await connection.QueryFirstOrDefaultAsync<T>(query, parameters);
    }
    
    protected override string BuildConnectionString(DatabaseConfig config)
    {
        return $"Server={config.Host};Database={config.Database};Trusted_Connection=true;";
    }
    
    // Override virtual method for SQL Server specific logging
    protected override async Task OpenConnectionAsync(IDbConnection connection)
    {
        await base.OpenConnectionAsync(connection);
        Logger.LogDebug($"Connected to SQL Server database: {connection.Database}");
    }
}

public class MySqlProvider : DatabaseProvider
{
    public MySqlProvider(string connectionString, ILogger logger) 
        : base(connectionString, logger) { }
    
    protected override IDbConnection CreateConnection()
    {
        return new MySqlConnection(ConnectionString);
    }
    
    protected override async Task<T> ExecuteQueryInternalAsync<T>(IDbConnection connection, string query, object parameters)
    {
        // MySQL specific implementation
        return await connection.QueryFirstOrDefaultAsync<T>(query, parameters);
    }
    
    protected override string BuildConnectionString(DatabaseConfig config)
    {
        return $"Server={config.Host};Database={config.Database};Uid={config.Username};Pwd={config.Password};";
    }
}

Class Hierarchy Visualization

Here’s how the inheritance hierarchy looks:

UML class diagram showing the abstract DatabaseProvider base class with its protected members and abstract methods, and two concrete implementations: SqlServerProvider and MySqlProvider, each overriding the required methods.

DatabaseProvider is an abstract base class for database providers, with SqlServerProvider and MySqlProvider inheriting and implementing provider-specific logic. This illustrates the template method pattern and shared behavior in C# abstract classes.

When to Use Abstract Classes

Abstract classes work best in these scenarios:

Framework Development: When you’re building a framework or SDK where you want to provide a consistent API while allowing customization. The abstract base class defines the workflow, and concrete classes handle the specifics.

Template Method Pattern: When you have a multi-step process where the overall flow is consistent but individual steps vary. The abstract class defines the template, and derived classes fill in the details.

Shared State and Behavior: When related classes need to share both data (fields, properties) and behavior (implemented methods). Abstract classes can hold state, unlike other alternatives.

Enforcing Implementation: When you need to guarantee that certain methods are implemented by all subclasses, but you also want to provide some shared functionality.

Common Mistakes and How to Avoid Them

Mistake 1: Making everything abstract Don’t make methods abstract unless they truly need different implementations in each subclass. If you can provide a reasonable default implementation, use virtual methods instead.

// Bad - this could have a default implementation
public abstract void LogOperation();

// Better - provide default, allow override
public virtual void LogOperation()
{
    Console.WriteLine($"Operation completed on {DateTime.Now}");
}

Mistake 2: Abstract classes with no abstract members If your abstract class has no abstract methods or properties, consider whether it should be abstract at all. Maybe a regular base class with virtual methods would be more appropriate.

Mistake 3: Trying to instantiate abstract classes Remember that abstract classes cannot be instantiated directly. You’ll get a compile-time error if you try.

// This won't compile
var provider = new DatabaseProvider(connectionString, logger);

// This is correct
var provider = new SqlServerProvider(connectionString, logger);

Mistake 4: Not calling base class constructors Abstract classes often have constructors that derived classes must call. Always ensure your derived class constructors properly initialize the base class.

Understanding the Limitations

Abstract classes in C# have some important limitations you should understand:

Single Inheritance: A class can only inherit from one abstract class. C# doesn’t support multiple inheritance for classes, so choose your inheritance hierarchy carefully.

Cannot Be Sealed: You cannot mark an abstract class as sealed since it’s designed to be inherited.

Constructors Are Not Inherited: While derived classes must call base constructors, they don’t automatically inherit them. You need to explicitly define constructors in your derived classes.

Abstract classes are powerful tools for creating well-structured, maintainable code. They help you share implementation while enforcing contracts, making them perfect for frameworks, template patterns, and scenarios where you need both shared behavior and guaranteed implementation.

The key is knowing when to use them. When you find yourself wanting to share code between related classes while ensuring certain methods are always implemented, abstract classes are likely your best choice. They provide the structure and flexibility needed to build robust, extensible systems that other developers can easily understand and extend.

Frequently Asked Questions

What’s the difference between abstract methods and virtual methods in C#?

In C#, abstract methods have no implementation and must be overridden in derived classes, while virtual methods provide a default implementation that can be optionally overridden. Abstract methods use the syntax public abstract void Method(); without any body, whereas virtual methods include both the declaration and implementation- public virtual void Method() { /* code */ }. This distinction lets you balance between enforcing implementation and providing useful defaults in your class hierarchy.

Can abstract classes implement interfaces in C#?

Yes, abstract classes in C# can implement interfaces, combining the contract enforcement of interfaces with the shared implementation of abstract classes. When an abstract class implements an interface, it can either implement all interface methods concretely, mark some as abstract to force derived classes to implement them, or use a mix of both approaches. This gives you flexibility when designing your inheritance hierarchy with both contract specifications and shared code. See here for Abstract Class vs Interface in C#- with Real-World Examples, and When to Use Them

What is the Template Method pattern and how do abstract classes help implement it?

The Template Method pattern uses an abstract class to define the skeleton of an algorithm while letting subclasses override specific steps. In C#, you can implement this by creating an abstract base class with a non-virtual public method that calls various protected virtual or protected abstract methods. This ensures the algorithm’s structure remains consistent across all implementations while allowing customization of individual steps, as shown in your DatabaseProvider.ExecuteQueryAsync<T> example.

How do constructors work in abstract classes in C#?

Constructors in abstract classes initialize shared state that all derived classes need, even though you can’t instantiate the abstract class directly. When creating a derived class, you must call the base class constructor using the base(...) syntax in C#. You can have multiple constructors in abstract classes, including parameter-less ones, and they can enforce the proper initialization of required fields through constructor chaining.

What’s the performance impact of using abstract classes instead of interfaces in C#?

Using abstract classes in C# has minimal performance difference compared to interfaces in most scenarios, especially with modern JIT optimizations. Abstract method calls use virtual method tables like interface methods, though abstract classes may have a slight memory advantage when sharing implementation code. The choice between abstract classes and interfaces should be based on design considerations rather than performance, focusing on whether you need shared code (abstract class) or multiple inheritance capability (interface).

What’s the difference between abstract methods and virtual methods in C#?

An abstract method has no implementation and must be overridden in derived classes, while a virtual method provides a default implementation that can be optionally overridden. Abstract methods use the syntax public abstract void Method(); without a body, whereas virtual methods include both declaration and implementation. This distinction allows you to balance between enforcing implementation requirements and providing useful defaults in your class hierarchy.

What is the Template Method pattern and how do abstract classes enable it?

The Template Method pattern uses an abstract class to define the skeleton of an algorithm while letting subclasses override specific steps. In C#, implement this by creating an abstract base class with a non-virtual public method (like ExecuteQueryAsync<T>) that calls various protected virtual or protected abstract methods. This ensures the algorithm’s structure remains consistent across all implementations while allowing customization of individual steps.

How do I decide between making a method abstract versus virtual in C#?

Make a method abstract when derived classes must provide their own implementation and no reasonable default exists. Use virtual when you have a sensible default implementation but want to allow customization. Consider whether forcing implementation (abstract) or providing flexibility (virtual) better serves your design goals and the likelihood of needing unique implementations in derived classes.

Why would I use an abstract class instead of a static utility class?

Unlike static utility classes, abstract classes can participate in inheritance hierarchies and maintain state across method calls. Abstract classes can implement interfaces and allow polymorphic behavior through override methods. They provide a way to share both implementation and state while enforcing certain method implementations, making them ideal for template patterns and framework development.
See other c-sharp posts