The Specification Pattern: A Versatile Tool for Building Maintainable and Testable Software

The Specification Pattern: A Versatile Tool for Building Maintainable and Testable Software

When developing complex software systems, one of the key challenges is implementing flexible, reusable, and maintainable business rules and query logic. The Specification Pattern is a powerful tool that addresses this challenge by encapsulating query logic and business rules into reusable, combinable objects. This pattern is particularly valuable in scenarios where the rules governing your domain can change frequently or where you need to apply varying criteria across different parts of your system.

In this blog post, we’ll dive into the Specification Pattern, explore how it can be implemented using C#, and discuss its applications in both the Repository Pattern and a rules engine. We’ll also touch on the importance of testability when using this pattern.

What is the Specification Pattern?

The Specification Pattern is a design pattern that allows you to encapsulate business logic into reusable and combinable objects called specifications. Each specification represents a specific rule or query criterion. These specifications can then be combined using logical operators (e.g., AND, OR, NOT) to create more complex criteria.

This pattern is particularly useful when you need to apply different sets of rules or query criteria across various contexts in your application. By encapsulating these rules, the Specification Pattern promotes code reuse, reduces duplication, and makes your code easier to maintain.

Implementing the Specification Pattern in C#

Below is an implementation of the Specification Pattern using C#. This example defines an abstract Specification<T> class that serves as the base class for all specifications. It provides a clear and flexible way to define query criteria, ordering, eager loading, and pagination.

public abstract class Specification<T> : ISpecification<T>
{
    private readonly List<Expression<Func<T, object>>> _includes = [];
    private readonly List<string> _includeStrings = [];
    private readonly List<Expression<Func<T, object>>> _thenBys = [];
    private readonly List<Expression<Func<T, object>>> _thenBysDescending = [];

    public abstract Expression<Func<T, bool>> Criteria { get; }

    public IReadOnlyList<Expression<Func<T, object>>> Includes => _includes.AsReadOnly();

    public IReadOnlyList<string> IncludeStrings => _includeStrings.AsReadOnly();

    public IReadOnlyList<Expression<Func<T, object>>> ThenBys => _thenBys.AsReadOnly();

    public IReadOnlyList<Expression<Func<T, object>>> ThenBysDescending => _thenBysDescending.AsReadOnly();

    public Expression<Func<T, object>>? OrderBy { get; private set; }

    public Expression<Func<T, object>>? OrderByDescending { get; private set; }

    public Expression<Func<T, object>>? GroupBy { get; private set; }

    public int? Skip { get; private set; }

    public int? Take { get; private set; }

    public bool IsPagingEnabled => Skip.HasValue && Take.HasValue;

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Throw.IfNull(includeExpression);
        _includes.Add(includeExpression);
    }

    protected virtual void AddInclude(string includeString)
    {
        Throw.IfNullOrEmpty(includeString);
        _includeStrings.Add(includeString);
    }

    protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
    {
        if (OrderByDescending is not null)
        {
            Throw.InvalidOperationException("OrderByDescending is already set. Only one primary ordering is allowed.");
        }

        if (orderByExpression == null && (_thenBys.Count > 0 || _thenBysDescending.Count > 0))
        {
            Throw.InvalidOperationException("Cannot remove OrderBy when ThenBy clauses are present.");
        }

        OrderBy = orderByExpression;
    }

    protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression)
    {
        if (OrderBy is not null)
        {
            Throw.InvalidOperationException("OrderBy is already set. Only one primary ordering is allowed.");
        }

        if (orderByDescendingExpression == null && (_thenBys.Count > 0 || _thenBysDescending.Count > 0))
        {
            Throw.InvalidOperationException("Cannot remove OrderByDescending when ThenBy clauses are present.");
        }

        OrderByDescending = orderByDescendingExpression;
    }

    protected virtual void AddThenBy(Expression<Func<T, object>> thenByExpression)
    {
        if (OrderBy == null && OrderByDescending == null)
        {
            Throw.InvalidOperationException("OrderBy or OrderByDescending must be applied before adding a ThenBy clause.");
        }

        _thenBys.Add(thenByExpression);
    }

    protected virtual void AddThenByDescending(Expression<Func<T, object>> thenByDescendingExpression)
    {
        if (OrderBy == null && OrderByDescending == null)
        {
            Throw.InvalidOperationException("OrderBy or OrderByDescending must be applied before adding a ThenByDescending clause.");
        }

        _thenBysDescending.Add(thenByDescendingExpression);
    }

    protected virtual void ApplyGroupBy(Expression<Func<T, object>> groupByExpression)
    {
        GroupBy = groupByExpression;
    }

    protected virtual void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
    }
}

Applications of the Specification Pattern

1. Using the Specification Pattern in the Repository Pattern

The Repository Pattern is commonly used to abstract data access logic from the business logic. By combining the Specification Pattern with the Repository Pattern, you can cleanly separate query logic from data access code. This allows for greater flexibility and reusability.

For example, consider a repository that manages Order entities. Instead of hardcoding the filtering logic within the repository, you can pass in a Specification<Order> object that defines the criteria for querying orders. This way, the repository becomes more flexible, and you can reuse the same specification across different parts of your application.

public interface IOrderRepository
{
    IEnumerable<Order> Find(ISpecification<Order> specification);
}

public class OrderRepository : IOrderRepository
{
    private readonly DbContext _context;

    public OrderRepository(DbContext context)
    {
        _context = context;
    }

    public IEnumerable<Order> Find(ISpecification<Order> specification)
    {
        IQueryable<Order> query = _context.Set<Order>();

        if (specification.Criteria is not null)
        {
            query = query.Where(specification.Criteria);
        }

        foreach (var include in specification.Includes)
        {
            query = query.Include(include);
        }

        foreach (var includeString in specification.IncludeStrings)
        {
            query = query.Include(includeString);
        }

        if (specification.OrderBy is not null)
        {
            query = query.OrderBy(specification.OrderBy);
        }
        else if (specification.OrderByDescending is not null)
        {
            query = query.OrderByDescending(specification.OrderByDescending);
        }

        foreach (var thenBy in specification.ThenBys)
        {
            query = query.ThenBy(thenBy);
        }

        foreach (var thenByDescending in specification.ThenBysDescending)
        {
            query = query.ThenByDescending(thenByDescending);
        }

        if (specification.IsPagingEnabled)
        {
            query = query.Skip(specification.Skip.Value).Take(specification.Take.Value);
        }

        return query.ToList();
    }
}

By using the Specification Pattern, the repository remains generic and can accommodate various query requirements without changing its implementation.

2. Using the Specification Pattern in a Rules Engine

The Specification Pattern is also well-suited for implementing rules engines. In many business applications, you need to evaluate complex sets of rules to determine outcomes, such as loan approvals, insurance claims, or order discounts. Specifications allow you to encapsulate each rule in a reusable object, making it easy to combine and apply rules dynamically.

For example, consider a rules engine that determines whether a customer qualifies for a discount:

public class EligibleForDiscountSpecification : Specification<Customer>
{
    public override Expression<Func<Customer, bool>> Criteria =>
        customer => customer.TotalPurchases > 1000 && customer.IsLoyalCustomer;
}

public class RecentlyPurchasedSpecification : Specification<Customer>
{
    public override Expression<Func<Customer, bool>> Criteria =>
        customer => customer.LastPurchaseDate >= DateTime.UtcNow.AddMonths(-3);
}

You can then combine these specifications to create more complex rules:

var discountEligibilitySpec = new EligibleForDiscountSpecification()
    .And(new RecentlyPurchasedSpecification());

var eligibleCustomers = customerRepository.Find(discountEligibilitySpec);

This approach allows you to easily add, modify, or remove rules without affecting the rest of your system.

Testability

One of the significant advantages of the Specification Pattern is its inherent testability. Because specifications encapsulate query logic in isolated, reusable objects, they can be easily tested independently of other parts of the system. Unit tests for specifications can focus on ensuring that the criteria are correctly applied, without needing to involve the database or other dependencies.

Here’s an example of a unit test for a specification:

[TestClass]
public class EligibleForDiscountSpecificationTests
{
    [TestMethod]
    public void ShouldReturnTrueForEligibleCustomer()
    {
        var spec = new EligibleForDiscountSpecification();
        var customer = new Customer { TotalPurchases = 1500, IsLoyalCustomer = true };

        var isSatisfied = spec.Criteria.Compile().Invoke(customer);

        Assert.IsTrue(isSatisfied);
    }

    [TestMethod]
    public void ShouldReturnFalseForIneligibleCustomer()
    {
        var spec = new EligibleForDiscountSpecification();
        var customer = new Customer { TotalPurchases = 500, IsLoyalCustomer = false };

        var isSatisfied = spec.Criteria.Compile().Invoke(customer);

        Assert.IsFalse(isSatisfied);
    }
}

Wrapping Up

The Specification Pattern is a versatile and powerful tool that enhances both the maintainability and flexibility of your codebase. When combined with the Repository Pattern, it provides a clean and reusable way to encapsulate query logic. In a rules engine, it allows for the dynamic composition of business rules, making your system adaptable to changing requirements.

Moreover, the Specification Pattern’s emphasis on encapsulation and separation of concerns makes your code more testable, ensuring that your business rules can be validated independently of the rest of the system.

Whether you’re managing complex query logic, building a rules engine, or simply looking to improve the organization of your code, the Specification Pattern offers a robust solution that can be applied across various scenarios in software design.