The Specification Pattern in .NET: Composable Query Logic
As applications grow, query logic tends to scatter. The same filters appear in repositories, services, and controllers, each with slight variations. The specification pattern gives you a way to encapsulate query criteria into reusable, composable objects.
The Core Idea
A specification is an object that defines a condition an entity must satisfy. Instead of scattering Where clauses throughout your codebase, you define each criterion once and compose them as needed.
A Simple Specification Base
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
var predicate = ToExpression().Compile();
return predicate(entity);
}
}
This gives you two capabilities: compile-time expressions for database queries (via EF Core's LINQ translation) and in-memory evaluation when needed.
Writing Specifications
public class ActiveOrdersSpec : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression() =>
order => order.Status != OrderStatus.Cancelled
&& order.Status != OrderStatus.Completed;
}
public class OrdersByCustomerSpec : Specification<Order>
{
private readonly Guid _customerId;
public OrdersByCustomerSpec(Guid customerId) => _customerId = customerId;
public override Expression<Func<Order, bool>> ToExpression() =>
order => order.CustomerId == _customerId;
}
public class HighValueOrdersSpec : Specification<Order>
{
private readonly decimal _minimumTotal;
public HighValueOrdersSpec(decimal minimumTotal) => _minimumTotal = minimumTotal;
public override Expression<Func<Order, bool>> ToExpression() =>
order => order.Total >= _minimumTotal;
}
Each specification has a single, clear responsibility.
Composing Specifications
The real power is composition. Add And and Or combinators:
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public Specification<T> And(Specification<T> other) =>
new AndSpecification<T>(this, other);
public Specification<T> Or(Specification<T> other) =>
new OrSpecification<T>(this, other);
public bool IsSatisfiedBy(T entity) =>
ToExpression().Compile()(entity);
}
internal class AndSpecification<T> : Specification<T>
{
private readonly Specification<T> _left;
private readonly Specification<T> _right;
public AndSpecification(Specification<T> left, Specification<T> right)
{
_left = left;
_right = right;
}
public override Expression<Func<T, bool>> ToExpression()
{
var leftExpr = _left.ToExpression();
var rightExpr = _right.ToExpression();
var parameter = Expression.Parameter(typeof(T));
var combined = Expression.AndAlso(
Expression.Invoke(leftExpr, parameter),
Expression.Invoke(rightExpr, parameter));
return Expression.Lambda<Func<T, bool>>(combined, parameter);
}
}
Now you can compose:
var spec = new ActiveOrdersSpec()
.And(new OrdersByCustomerSpec(customerId))
.And(new HighValueOrdersSpec(500m));
Using with a Repository
Extend your repository to accept specifications:
public interface IRepository<T> where T : class
{
Task<IReadOnlyList<T>> ListAsync(Specification<T> spec, CancellationToken ct = default);
Task<T?> FirstOrDefaultAsync(Specification<T> spec, CancellationToken ct = default);
Task<int> CountAsync(Specification<T> spec, CancellationToken ct = default);
}
public class EfRepository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _db;
public EfRepository(AppDbContext db) => _db = db;
public async Task<IReadOnlyList<T>> ListAsync(
Specification<T> spec, CancellationToken ct) =>
await _db.Set<T>()
.Where(spec.ToExpression())
.ToListAsync(ct);
public async Task<T?> FirstOrDefaultAsync(
Specification<T> spec, CancellationToken ct) =>
await _db.Set<T>()
.FirstOrDefaultAsync(spec.ToExpression(), ct);
public async Task<int> CountAsync(
Specification<T> spec, CancellationToken ct) =>
await _db.Set<T>()
.CountAsync(spec.ToExpression(), ct);
}
Using Ardalis.Specification
For production use, consider the Ardalis.Specification library, which handles expression composition, includes, ordering, and pagination:
public class ActiveHighValueOrdersSpec : Specification<Order>
{
public ActiveHighValueOrdersSpec(decimal minimumTotal)
{
Query
.Where(o => o.Status != OrderStatus.Cancelled)
.Where(o => o.Total >= minimumTotal)
.Include(o => o.Lines)
.OrderByDescending(o => o.CreatedAt);
}
}
This integrates directly with EF Core and handles all the expression tree plumbing.
When to Use Specifications
Specifications shine when you have complex filtering logic that's reused across multiple queries, when business rules determine data access (e.g., "active orders for this customer above this threshold"), or when you want to unit test query criteria without a database.
For simple applications with straightforward queries, inline LINQ is perfectly fine. Specifications add value when query complexity grows and the same criteria appear in multiple places. They turn implicit, scattered knowledge into explicit, reusable objects.