Base Class

What is a Base Class?

A base class is a class that other classes can inherit from - think of it as a template or foundation that provides common functionality to its derived classes. In C#, we use inheritance to create relationships between classes where one class (the derived class) extends another class (the base class).

Here’s the simplest example:

// Base class
public class Animal
{
    public string Name { get; set; }
    
    public virtual void MakeSound()
    {
        Console.WriteLine("Some generic animal sound");
    }
}

// Derived class
public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} says Woof!");
    }
}

The Dog class inherits from Animal, gaining access to the Name property and the ability to override the MakeSound() method.

Why Use Base Classes?

Base classes solve several key problems in software development:

1. Code Reuse

Instead of duplicating common functionality across multiple classes, you define it once in a base class:

// Without base class - lots of duplication
public class User
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    // User-specific properties...
}

public class Product
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    // Product-specific properties...
}

// With base class - no duplication
public abstract class BaseEntity
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
}

public class User : BaseEntity
{
    public string Email { get; set; }
    public string FirstName { get; set; }
    // Only User-specific properties here
}

public class Product : BaseEntity
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    // Only Product-specific properties here
}

2. Polymorphism

Base classes enable you to treat different types uniformly:

public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Polymorphism in action
public double CalculateTotalArea(List<Shape> shapes)
{
    double total = 0;
    foreach (Shape shape in shapes)
    {
        total += shape.CalculateArea(); // Calls the correct method for each type
    }
    return total;
}

Base Classes in API Development

Entity Base Classes

In API projects, base classes are commonly used for database entities:

public abstract class AuditableEntity
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public string CreatedBy { get; set; }
    public string UpdatedBy { get; set; }
    public bool IsDeleted { get; set; }
}

public class Customer : AuditableEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public List<Order> Orders { get; set; } = new();
}

public class Product : AuditableEntity
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

Controller Base Classes

You can create base controllers for common functionality:

[ApiController]
public abstract class BaseApiController : ControllerBase
{
    protected readonly ILogger Logger;

    protected BaseApiController(ILogger logger)
    {
        Logger = logger;
    }

    protected ActionResult HandleResult<T>(T result)
    {
        if (result == null)
            return NotFound();
            
        return Ok(result);
    }

    protected ActionResult HandleError(Exception ex, string operation)
    {
        Logger.LogError(ex, "Error during {Operation}", operation);
        return StatusCode(500, "An error occurred");
    }
}

[Route("api/[controller]")]
public class ProductsController : BaseApiController
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService, ILogger<ProductsController> logger) 
        : base(logger)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult> GetProduct(Guid id)
    {
        try
        {
            var product = await _productService.GetByIdAsync(id);
            return HandleResult(product); // Using base class method
        }
        catch (Exception ex)
        {
            return HandleError(ex, nameof(GetProduct)); // Using base class method
        }
    }
}

Service Base Classes

Common service functionality can be extracted to base classes:

public abstract class BaseService<T> where T : AuditableEntity
{
    protected readonly AppDbContext Context;
    protected readonly ILogger Logger;

    protected BaseService(AppDbContext context, ILogger logger)
    {
        Context = context;
        Logger = logger;
    }

    public virtual async Task<T?> GetByIdAsync(Guid id)
    {
        return await Context.Set<T>()
            .Where(e => !e.IsDeleted)
            .FirstOrDefaultAsync(e => e.Id == id);
    }

    public virtual async Task<IEnumerable<T>> GetAllAsync()
    {
        return await Context.Set<T>()
            .Where(e => !e.IsDeleted)
            .ToListAsync();
    }

    protected virtual void SetAuditFields(T entity, string userId, bool isUpdate = false)
    {
        if (!isUpdate)
        {
            entity.Id = Guid.NewGuid();
            entity.CreatedAt = DateTime.UtcNow;
            entity.CreatedBy = userId;
        }
        
        entity.UpdatedAt = DateTime.UtcNow;
        entity.UpdatedBy = userId;
    }
}

public class ProductService : BaseService<Product>
{
    public ProductService(AppDbContext context, ILogger<ProductService> logger) 
        : base(context, logger)
    {
    }

    public async Task<Product> CreateAsync(CreateProductRequest request, string userId)
    {
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price
        };

        SetAuditFields(product, userId); // Using base class method

        Context.Products.Add(product);
        await Context.SaveChangesAsync();
        
        return product;
    }
}

Virtual vs Abstract Members

Understanding the difference is crucial:

Virtual Members

Can be overridden but have a default implementation:

public class BaseRepository<T>
{
    public virtual async Task<T> SaveAsync(T entity)
    {
        // Default implementation
        Context.Set<T>().Add(entity);
        await Context.SaveChangesAsync();
        return entity;
    }
}

public class UserRepository : BaseRepository<User>
{
    // Can choose to override or use the default implementation
    public override async Task<User> SaveAsync(User user)
    {
        // Custom validation for users
        ValidateUser(user);
        
        // Call base implementation
        return await base.SaveAsync(user);
    }
}

Abstract Members

Must be implemented by derived classes:

public abstract class NotificationService
{
    // Must be implemented by derived classes
    public abstract Task SendAsync(string recipient, string message);

    // Can provide common functionality
    public virtual string FormatMessage(string template, Dictionary<string, string> parameters)
    {
        // Common message formatting logic
        foreach (var param in parameters)
        {
            template = template.Replace($"{{{param.Key}}}", param.Value);
        }
        return template;
    }
}

public class EmailService : NotificationService
{
    public override async Task SendAsync(string recipient, string message)
    {
        // Email-specific implementation required
        await SendEmailAsync(recipient, message);
    }
}

public class SmsService : NotificationService
{
    public override async Task SendAsync(string recipient, string message)
    {
        // SMS-specific implementation required
        await SendSmsAsync(recipient, message);
    }
}

Constructor Chaining

Derived classes can call base class constructors:

public abstract class BaseEntity
{
    public Guid Id { get; }
    public DateTime CreatedAt { get; }

    protected BaseEntity()
    {
        Id = Guid.NewGuid();
        CreatedAt = DateTime.UtcNow;
    }

    protected BaseEntity(Guid id) : this()
    {
        Id = id; // Override the generated ID
    }
}

public class Product : BaseEntity
{
    public string Name { get; set; }

    public Product(string name) : base() // Calls parameterless base constructor
    {
        Name = name;
    }

    public Product(Guid id, string name) : base(id) // Calls base constructor with ID
    {
        Name = name;
    }
}

Best Practices

1. Prefer Composition Over Inheritance

Don’t use inheritance just for code reuse. Ask yourself: “Is this truly an ‘is-a’ relationship?”

// Poor use of inheritance - Logger is not a type of User
public class User : Logger
{
    public string Name { get; set; }
}

// Better - composition
public class User
{
    private readonly ILogger _logger;
    
    public string Name { get; set; }

    public User(ILogger logger)
    {
        _logger = logger;
    }
}

2. Keep Inheritance Hierarchies Shallow

Avoid deep inheritance chains:

// Avoid this - too deep
public class Animal { }
public class Mammal : Animal { }
public class Carnivore : Mammal { }
public class Feline : Carnivore { }
public class BigCat : Feline { }
public class Lion : BigCat { }

// Prefer this - flatter hierarchy
public class Animal { }
public class Lion : Animal 
{
    // Use interfaces for behavior
    // Implement ICarnivore, IFeline, etc.
}

3. Use Abstract Base Classes for Framework Code

When building frameworks or libraries:

public abstract class RepositoryBase<T> where T : class
{
    protected readonly DbContext Context;

    protected RepositoryBase(DbContext context)
    {
        Context = context;
    }

    public virtual async Task<T?> GetByIdAsync(object id)
    {
        return await Context.Set<T>().FindAsync(id);
    }

    // Force derived classes to implement their own query logic
    public abstract Task<IQueryable<T>> GetQueryable();
}

4. Consider Sealed Classes

If you don’t intend for a class to be inherited from, make it sealed:

public sealed class EmailValidator
{
    public bool IsValid(string email)
    {
        // Validation logic
        return true;
    }
}

// This won't compile - cannot inherit from sealed class
// public class CustomEmailValidator : EmailValidator { }

Common Pitfalls

1. The Fragile Base Class Problem

Changes to base classes can break derived classes:

// Version 1
public class BaseService
{
    public virtual void ProcessData(string data)
    {
        // Simple processing
    }
}

// Version 2 - breaking change
public class BaseService
{
    public virtual void ProcessData(string data, bool validate = true)
    {
        // Now validates by default - might break derived classes
    }
}

2. Violating Liskov Substitution Principle

Derived classes should be substitutable for their base classes:

// Violates LSP - Rectangle behavior changes when treated as Shape
public class Shape
{
    public virtual void SetDimensions(double width, double height) { }
}

public class Rectangle : Shape
{
    public double Width { get; private set; }
    public double Height { get; private set; }

    public override void SetDimensions(double width, double height)
    {
        Width = width;
        Height = height;
    }
}

public class Square : Rectangle
{
    public override void SetDimensions(double width, double height)
    {
        // Breaks LSP - ignores height parameter
        Width = Height = width;
    }
}

Wrap Up

Base classes are a powerful tool in C# that enable code reuse, polymorphism, and clean architectural patterns. They’re particularly useful in API development for creating consistent entity models, controller patterns, and service abstractions.

The key is knowing when to use them - they should represent true “is-a” relationships and provide meaningful shared behavior. When used appropriately, base classes can significantly reduce code duplication and create more maintainable, extensible systems.

Remember: inheritance is just one tool in your OOP toolbox. Sometimes composition, interfaces, or other patterns might be more appropriate for your specific scenario.