.NETHard
Specification pattern in .NET — composable, testable business rules
The Specification pattern encapsulates a business rule as an object you can combine, store, and test in isolation.
The shape
public interface ISpecification<T> {
Expression<Func<T, bool>> ToExpression();
bool IsSatisfiedBy(T candidate);
}
Expression so EF Core can translate it to SQL.
A real rule
public class PremiumCustomerSpec : ISpecification<Customer> {
public Expression<Func<Customer, bool>> ToExpression() =>
c => c.TotalSpend >= 100_000 && c.JoinedAt < DateTime.UtcNow.AddYears(-1);
public bool IsSatisfiedBy(Customer c) => ToExpression().Compile()(c);
}
Same spec, two consumers:
// EF Core — translated to SQL
var premium = await db.Customers.Where(spec.ToExpression()).ToListAsync();
// In-memory check
if (spec.IsSatisfiedBy(customer)) SendVipEmail(customer);
Composition operators
public sealed class AndSpec<T>(ISpecification<T> a, ISpecification<T> b) : ISpecification<T> {
public Expression<Func<T, bool>> ToExpression() {
var p = Expression.Parameter(typeof(T));
var body = Expression.AndAlso(
Expression.Invoke(a.ToExpression(), p),
Expression.Invoke(b.ToExpression(), p));
return Expression.Lambda<Func<T, bool>>(body, p);
}
public bool IsSatisfiedBy(T c) => a.IsSatisfiedBy(c) && b.IsSatisfiedBy(c);
}
// Combine
var spec = new PremiumCustomerSpec().And(new ActiveInLast30DaysSpec());
Where it pays
- Reusable business filters. The same "active premium customer" rule appears in 5 controllers — define it once.
- Unit-testable rules.
Assert.True(new PremiumCustomerSpec().IsSatisfiedBy(fixture)); - Storable rules. Marketing wants to define an audience: persist the spec (or a DSL that translates to one) and apply it later.
- Compile-safe query reuse with EF Core.
When it does not pay
- One-shot LINQ filter used in one place — inlined
Where(c => ...)wins on readability. - Rules with many parameters — they become awkward as classes with state. A static method that returns an
Expression<Func<T, bool>>is often clearer.
Lighter-weight alternative
Static factory methods that return Expression<Func<T, bool>>:
public static class CustomerSpecs {
public static Expression<Func<Customer, bool>> IsPremium() =>
c => c.TotalSpend >= 100_000;
}
var premium = await db.Customers.Where(CustomerSpecs.IsPremium()).ToListAsync();
Gives you 80% of the pattern without the type ceremony. Worth knowing when you want testable, composable, EF-translatable filters without a full Specification framework.