Skip to content

Specification vs Alternatives

The Specification pattern is not the only way to manage business rules. You can also pass lambda expressions directly, use the Query Object pattern, or combine LINQ extension methods. So when should you choose Specification? This appendix compares the major alternatives with the Specification pattern and summarizes the appropriate use scenarios for each.


A quick comparison of the pros, cons, and suitable scenarios for five approaches.

ApproachProsConsSuitable For
Inline PredicateSimple, no extra classes neededNot reusable, hard to testOne-off filters, simple conditions
Strategy PatternAlgorithm swappingNo composition, overkill for bool returnsWhen algorithm swapping is the goal
Repository-per-QueryIntuitive, type-safeMethod explosion, duplicate codeWhen condition combinations are few
Dynamic LINQFlexible string-based queriesNo compile-time validation, security risksAdmin ad-hoc queries
Specification PatternComposable, reusable, easy to testInitial implementation cost, learning curveComplex business rules, DDD

// Inline predicate
var activeProducts = products.Where(p => p.IsActive && !p.IsDiscontinued);

Pros:

  • Immediate use without extra classes
  • Suitable for simple conditions
  • No learning cost

Cons:

  • Same condition repeated in multiple places
  • All usages must be modified when rules change
  • Cannot assign a name to the condition
  • Cannot independently verify just the condition via unit tests

public interface IProductFilter
{
IEnumerable<Product> Filter(IEnumerable<Product> products);
}
public class ActiveProductFilter : IProductFilter
{
public IEnumerable<Product> Filter(IEnumerable<Product> products) =>
products.Where(p => p.IsActive);
}

Pros:

  • Encapsulates filter logic
  • Filters can be swapped at runtime

Cons:

  • Operates on the entire collection (not individual item evaluation)
  • No standard way to combine two Strategies with And/Or
  • Cannot convert to Expression Tree

public interface IProductRepository
{
Task<List<Product>> GetActiveProductsAsync();
Task<List<Product>> GetActiveProductsByCategoryAsync(string category);
Task<List<Product>> GetPremiumActiveProductsByCategoryAsync(string category);
// ... new method for each combination
}

Pros:

  • Intuitive and type-safe
  • IDE auto-completion support

Cons:

  • N conditions -> up to 2^N methods
  • Condition logic scattered across Repository implementations
  • Interface changes required when adding new conditions

// String-based dynamic query (System.Linq.Dynamic.Core)
var result = products.AsQueryable()
.Where("IsActive == true && Price > @0", minPrice);

Pros:

  • Very flexible runtime query construction
  • Suitable for user-defined filters

Cons:

  • No compile-time type validation
  • Runtime errors from typos
  • Security risks similar to SQL injection
  • Difficult to track strings during refactoring

var spec = new ActiveProductSpec() & new ProductCategorySpec("Electronics");
var products = await repository.FindAsync(spec);

Pros:

  • Assigns names to business rules (ubiquitous language)
  • Free composition with And, Or, Not
  • Individual Specifications can be tested independently
  • ORM integration through Expression Trees
  • Prevents Repository method explosion

Cons:

  • Initial framework implementation cost (resolved by using Functorium)
  • Can be overkill for simple conditions
  • Requires understanding Expression Trees (covered in Part 2)

Is the condition used in only one place?
├── Yes -> Inline Predicate
└── No -> Is condition composition needed?
├── No -> Strategy Pattern or Repository-per-Query
└── Yes -> Is ORM integration needed?
├── No -> Specification (memory-based)
└── Yes -> ExpressionSpecification (Expression Tree)

Check the anti-patterns of the Specification pattern.

-> B. Anti-Patterns