Reusable Query Logic with the Specification Pattern
In this lesson, you will learn how to encapsulate complex business rules into reusable objects to keep your data-access layer clean and expressive.
When building applications, you often find yourself repeating the same filtering logic—like finding "active users" or "overdue orders"—across different repositories or services. Instead of scattering LINQ expressions everywhere, the Specification Pattern allows you to wrap that logic into a dedicated, generic class.
Building a Generic Specification
The goal is to create a structure that describes a rule. By leveraging C# generics and Expression trees (which we touched on in lesson 20), we can create a base interface that enforces a consistent structure for our specifications.
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
bool IsSatisfiedBy(T entity);
}
By implementing this interface, you create an object that defines exactly what it means for an entity to "match." For example:
public class ActiveUserSpecification : ISpecification<User>
{
public Expression<Func<T, bool>> Criteria => user => user.Status == Status.Active;
public bool IsSatisfiedBy(User user) => user.Status == Status.Active;
}
Why use this architecture?
By encapsulating logic in these specification objects, you gain three immediate architectural benefits:
- Reusability: Use the same
ActiveUserSpecificationin yourRepository(to filter database queries) and in yourDomain Logic(to validate business rules). - Testability: Because the specification is a standalone class, you can unit test your business rules in total isolation from the database or external services.
- Combinability: You can expand this architecture to include
AndSpecificationorOrSpecificationdecorators (using the Decorator pattern from lesson 21), allowing you to chain simple rules together to build complex, dynamic queries without manually concatenating strings or LINQ calls.
This approach transforms your "raw" data access into a declarative language where your code reads like your business requirements, rather than a collection of implementation details.
Key Takeaway: The Specification Pattern turns fragmented business rules into reusable objects, promoting clean, testable, and composable query logic across your entire architecture.