👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Specification Pattern - Eliminating Scattered Business Logic in DDD

Specification Pattern - Eliminating Scattered Business Logic in DDD

Author - Abdul Rahman (Bhai)

DDD

1 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

The Specification Pattern is a Domain-Driven Design pattern that encapsulates business rules and domain knowledge into dedicated, reusable objects. Introduced by Eric Evans and Martin Fowler, this pattern addresses a critical problem in enterprise software: scattered and duplicated business logic.

In this article, let's learn how to eliminate duplicated business logic using the Specification Pattern in .NET. We'll explore how to create reusable, testable business rules that work seamlessly with both Entity Framework Core queries and in-memory validation. You'll learn to define business rules once and reuse them consistently across validation, querying, and object creation.

Why we gonna do?

The Problem: Scattered Business Logic

Consider an e-commerce platform that needs to identify premium products. The business rule states: "Premium products must have a price above $100, belong to Electronics or Office categories, and have at least 5 units in stock."

Without specifications, this logic gets duplicated across multiple locations:

// In PremiumProductsController
var premiumProducts = products
    .Where(p => p.Price > 50 &&
        p.Stock > 5 &&
        (p.Category == "Electronics" || p.Category == "Office"))
    .ToList();

// In ValidationService
public bool IsPremiumProduct(Product product)
{
    return product.Price >= 100 &&
        product.Stock > 5 &&
        product.Category == "Industrial";
}

// In CatalogController
var featured = products
    .Where(p => p.Price > 75 &&
        p.Stock > 15 &&
        p.Category == "Consumer")
    .ToList();

Notice the problems:

  • Three different implementations - different price thresholds ($50, $100, $75)
  • Inconsistent stock requirements - 5 vs 15 units
  • Different categories - Electronics/Office vs Industrial vs Consumer
  • DRY principle violated - same business concept defined multiple ways

The Consequences

  1. Time Cost: Simple 5-minute changes become multi-day expeditions through the codebase
  2. Reliability Cost: Missing one location introduces subtle bugs that might not surface until production
  3. Velocity Cost: New developers spend excessive time understanding where business logic lives and how to maintain consistency

Why Traditional Approaches Fail

You might think extracting logic into static methods solves this. But here's the catch: Entity Framework Core can't translate regular methods into SQL. You need expression trees, not compiled code. And managing expression trees manually? That's complex and error-prone.

The Specification Pattern Advantage

The Specification Pattern gives you one source of truth for each business rule. Write it once, use it everywhere - for database queries, in-memory validation, and even test data generation. When requirements change, update exactly one place.

This pattern is essential for:

  • Enterprise applications with complex, evolving business rules
  • Teams needing consistent logic across multiple developers
  • Systems where business rules must work in both queries and validation
  • Codebases prioritizing maintainability and testability

How we gonna do?

Specification Pattern Solution

Let's see how the Specification Pattern transforms the scattered business logic problem:


// Business rule as a specification
public class PremiumProductSpecification : Specification<Product>
{
    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product => product.Price >= 100 &&
            product.Stock > 5 &&
            (product.Category == "Electronics" ||
            product.Category == "Office" ||
            product.Category == "Industrial");
    }
}

// Usage across different contexts
var spec = new PremiumProductSpecification();

// In controllers - database query
var premiumProducts = await repository.FindAsync(spec);

// In services - in-memory validation
if (spec.IsSatisfiedBy(product))
{
    ApplyPremiumDiscount(product);
}

Key benefits:

  • Single Source of Truth: Business logic defined exactly once
  • Reusable: Works for both database queries and in-memory validation
  • Maintainable: Change business rules in one place
  • Testable: Each specification can be tested independently
  • Composable: Combine specifications using logical operators

The Specification Pattern has two core goals:

  1. Eliminate Duplication: Follow the DRY principle by providing a single source of truth
  2. Enable Declarative Code: Express business concepts clearly, mirroring how domain experts communicate

Top Use Cases for Specification Pattern

The Specification Pattern excels in three primary use cases:

1. In-Memory Validation

Verify that objects meet business criteria while loaded in application memory, providing real-time enforcement of business rules.

var premiumSpec = new PremiumProductSpecification();

// Validate product in shopping cart
if (premiumSpec.IsSatisfiedBy(product))
{
    cart.ApplyPremiumDiscount();
}

// Same spec in pricing service
if (premiumSpec.IsSatisfiedBy(product))
{
    CalculatePremiumPrice(product);
}

Before: Validation logic duplicated across shopping cart, pricing service, and promotion engine, each with subtle variations.

After: Single specification used consistently everywhere, ensuring identical validation logic.

2. Building Database Queries

Use the same business logic for validation to generate efficient database queries, ensuring consistency between what you validate and what you query.

// Repository uses specification for database query
var premiumProducts = await repository.FindAsync(premiumSpec);

// Generated SQL (example):
// SELECT * FROM Products 
// WHERE Price >= 100 
//   AND Stock > 5 
//   AND Category IN ('Electronics', 'Office', 'Industrial')

Key Advantage: The specification generates optimized SQL automatically through Entity Framework Core, avoiding the need to load all data and filter in memory.

3. Guiding Object Creation

Ensure new objects satisfy specific business requirements, useful for recommendations and test data generation.

// Generate test premium products
var premiumSpec = new PremiumProductSpecification();
var testProducts = new List<Product>();

while (testProducts.Count < 5)
{
    var candidate = GenerateRandomProduct();
    if (premiumSpec.IsSatisfiedBy(candidate))
    {
        testProducts.Add(candidate);
    }
}

// Create product bundles
var bundleSpec = premiumSpec
    .And(new InStockSpecification(10))
    .And(new BusinessProductSpecification());
    
var bundleProducts = allProducts
    .Where(bundleSpec.IsSatisfiedBy)
    .Take(5)
    .ToList();

Avoiding Naive Implementations

Before diving into the proper implementation, let's examine five common anti-patterns that developers encounter:

Anti-Pattern 1: Direct Method Extraction

Extracting logic into static methods seems logical but fails with Entity Framework Core.

// This looks reasonable but fails at runtime
public static bool IsPremiumProduct(Product product)
{
    return product.Price >= 100 && product.Stock > 5;
}

// Usage in controller - throws exception
var products = context.Products
    .Where(p => IsPremiumProduct(p))  // ❌ Runtime error!
    .ToList();

// Error: "LINQ expression could not be translated"

Problem: Entity Framework needs expression trees to generate SQL. Compiled methods are "black boxes" that EF cannot translate.

Anti-Pattern 2: Plain Expression Trees Without Encapsulation

Using raw expression trees solves translation but creates other problems.

// Static expression tree
public static Expression<Func<Product, bool>> IsPremiumProduct = 
    product => product.Price >= 100 && product.Stock > 5;

// Database query works
var products = context.Products.Where(IsPremiumProduct).ToList();

// In-memory validation requires manual compilation
var compiledExpression = IsPremiumProduct.Compile();  // ⚠️ Expensive!
if (compiledExpression(product))
{
    // ...
}

Problems:

  • Poor encapsulation - consumers handle compilation
  • No caching - compilation happens every time
  • Technical details leak into business logic

Anti-Pattern 3: Client-Configured Specifications

Creating generic specifications that accept expressions from outside defeats the purpose.

// Generic specification - too flexible
public class GenericSpecification<T> : Specification<T>
{
    private readonly Expression<Func<T, bool>> _expression;
    
    public GenericSpecification(Expression<Func<T, bool>> expression)
    {
        _expression = expression;
    }
    
    protected override Expression<Func<T, bool>> ToExpression() => _expression;
}

// Usage - business logic still scattered
var spec1 = new GenericSpecification<Product>(p => p.Price > 50);
var spec2 = new GenericSpecification<Product>(p => p.Price >= 100);
var spec3 = new GenericSpecification<Product>(p => p.Price > 75);

Problem: Different developers create different implementations of the same business concept. The specification becomes just a thin wrapper with no real value.

Anti-Pattern 4: Repository Method Proliferation

Adding new repository methods for every business rule combination.

public class ProductRepository
{
    public List<Product> GetPremiumProducts() { }
    public List<Product> GetPremiumInStockProducts() { }
    public List<Product> GetPremiumBusinessProducts() { }
    public List<Product> GetPremiumInStockBusinessProducts() { }
    // ... hundreds more combinations
}

Problems: Massive repository classes, inconsistent implementations, and difficult maintenance.

Anti-Pattern 5: Configuration Overload

Making specifications overly configurable loses business meaning.

public class ConfigurableSpecification : Specification<Product>
{
    private readonly decimal? _minPrice;
    private readonly decimal? _maxPrice;
    private readonly int? _minStock;
    private readonly string[] _categories;
    
    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product =>
            (!_minPrice.HasValue || product.Price >= _minPrice) &&
            (!_maxPrice.HasValue || product.Price <= _maxPrice) &&
            (!_minStock.HasValue || product.Stock >= _minStock) &&
            (_categories == null || _categories.Contains(product.Category));
    }
}

Problem: Complex conditional logic, no clear business meaning, and difficult testing.

Understanding Expression Trees and LINQ

To properly implement specifications, we need to understand how LINQ works with expression trees.

The Where method in LINQ uses the same syntax but behaves differently depending on context:

// In-memory collection - uses Func<T, bool> (delegate)
List<Product> products = GetProducts();
var filtered = products.Where(p => p.Price > 100).ToList();

// Database query - uses Expression<Func<T, bool>> (expression tree)
IQueryable<Product> products = context.Products;
var filtered = products.Where(p => p.Price > 100).ToList();

The C# compiler makes an intelligent choice:

  • Delegate (Func<T, bool>): Compiled code ready to execute
  • Expression Tree: Data structure describing the code that can be analyzed and translated

Expression Trees vs Delegates

// Same lambda expression, different types
Func<Product, bool> priceDelegate = p => p.Price > 50;
Expression<Func<Product, bool>> priceExpression = p => p.Price > 50;

var product = new Product { Price = 100 };

// Delegate - direct execution
bool result1 = priceDelegate(product);  // true

// Expression - needs compilation
var compiled = priceExpression.Compile();
bool result2 = compiled(product);  // true

Key differences:

Aspect Delegate Expression Tree
Nature Compiled code Data structure describing code
Execution Runs immediately when called Needs compilation to execute
SQL Translation Cannot translate (black box) Can be analyzed and translated
Use Case In-memory operations Database queries + in-memory

How Entity Framework Translates Expression Trees

Entity Framework Core walks through expression trees and generates SQL:

// LINQ query with expression tree
var query = context.Products
    .Where(p => p.Price > 50 && p.Stock > 5);

// EF Core analyzes the expression tree:
// - Finds BinaryExpression for Price > 50
// - Finds BinaryExpression for Stock > 5
// - Finds AndAlso combining them

// Generated SQL:
// SELECT * FROM Products 
// WHERE Price > 50 AND Stock > 5

This transformation is why specifications work seamlessly with Entity Framework Core while maintaining type safety and testability.

Building the Base Specification Class

Now let's build a robust base specification class that addresses all the requirements we've identified.

Requirements

  1. Single Source of Truth: Business logic defined exactly once
  2. Optimal Performance: Automatic compilation caching
  3. Dual Purpose: Works for both EF Core queries and in-memory validation
  4. Hide Complexity: No technical details exposed to consumers
  5. Type Safety: Compile-time checks
  6. Handle Edge Cases: Null checks and validation
  7. Extensible: Support for composition

Base Specification Implementation


public abstract class Specification<T> where T : class
{
    // Cache compiled expression for performance
    private Func<T, bool>? _compiledExpression;

    // Derived classes implement business logic here
    protected abstract Expression<Func<T, bool>> ToExpression();

    // In-memory validation with automatic caching
    public bool IsSatisfiedBy(T entity)
    {
        if (entity == null)
            throw new ArgumentNullException(nameof(entity));

        // Compile once, cache for future calls
        _compiledExpression ??= ToExpression().Compile();
        
        return _compiledExpression(entity);
    }

    // Database query support for EF Core
    public Expression<Func<T, bool>> GetExpression()
    {
        return ToExpression();
    }

    // Composition support (covered later)
    public static Specification<T> All => new IdentitySpecification<T>();
    
    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 Specification<T> Not() => 
        new NotSpecification<T>(this);
}

This base class satisfies all our requirements:

  • Single Source: ToExpression() implemented once per concrete specification
  • Performance: Compilation happens once, cached in _compiledExpression
  • Dual Purpose: GetExpression() for EF Core, IsSatisfiedBy() for in-memory
  • Hidden Complexity: Only two public methods exposed
  • Type Safety: Generic constraint where T : class
  • Edge Cases: Null check in IsSatisfiedBy()
  • Extensible: Protected ToExpression() allows composition

Implementing Strongly-Typed Specifications

With our base class ready, let's implement concrete specifications representing real business rules.

Premium Product Specification


public class PremiumProductSpecification : Specification<Product>
{
    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product =>
            product.Price >= 100 &&
            product.Stock > 5 &&
            (product.Category == "Electronics" ||
            product.Category == "Office" ||
            product.Category == "Industrial");
    }
}

Clean and simple! The business logic lives in exactly one place. No duplication, no configuration complexity.

Parameterized Specification

Sometimes specifications need parameters:


public class InStockSpecification : Specification<Product>
{
    private readonly int _minimumStock;

    public InStockSpecification(int minimumStock)
    {
        _minimumStock = minimumStock;
    }

    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product => product.Stock >= _minimumStock;
    }
}

// Usage
var inStockSpec = new InStockSpecification(10);
var products = await repository.FindAsync(inStockSpec);

Business Product Specification


public class BusinessProductSpecification : Specification<Product>
{
    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product => 
            product.IsBusinessProduct &&
            product.MinimumOrderQuantity >= 10;
    }
}

Usage in Controllers

public class PremiumProductsController : ControllerBase
{
    private readonly IRepository<Product> _repository;

    [HttpGet]
    public async Task<IActionResult> GetPremiumProducts()
    {
        var spec = new PremiumProductSpecification();
        var products = await _repository.FindAsync(spec);
        
        return Ok(products);
    }

    [HttpGet("count")]
    public async Task<IActionResult> CountPremiumProducts()
    {
        var spec = new PremiumProductSpecification();
        var count = await _repository.CountAsync(spec);
        
        return Ok(new { count });
    }
}

Usage in Services

public class ProductValidationService
{
    public ValidationResult ValidateForPremiumDiscount(Product product)
    {
        var premiumSpec = new PremiumProductSpecification();
        
        if (!premiumSpec.IsSatisfiedBy(product))
        {
            return ValidationResult.Failure("Product does not qualify as premium");
        }
        
        return ValidationResult.Success();
    }
}

Notice how the same specification works in both contexts:

  • Controller uses FindAsync(spec) → Generates SQL query
  • Service uses IsSatisfiedBy(product) → In-memory validation
  • Both use identical business logic

Integrating with Entity Framework Core

To use specifications with Entity Framework Core efficiently, let's create a generic specification repository.

Specification Repository


public class SpecificationRepository<TContext, T> 
    where TContext : DbContext 
    where T : class
{
    private readonly TContext _context;
    private readonly DbSet<T> _dbSet;

    public SpecificationRepository(TContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    // Find entities matching specification
    public async Task<List<T>> FindAsync(Specification<T> specification)
    {
        return await _dbSet
            .Where(specification.GetExpression())
            .ToListAsync();
    }

    // Check if any entity matches
    public async Task<bool> AnyAsync(Specification<T> specification)
    {
        return await _dbSet
            .AnyAsync(specification.GetExpression());
    }

    // Count matching entities
    public async Task<int> CountAsync(Specification<T> specification)
    {
        return await _dbSet
            .CountAsync(specification.GetExpression());
    }

    // Find first matching entity
    public async Task<T?> FindSingleAsync(Specification<T> specification)
    {
        return await _dbSet
            .FirstOrDefaultAsync(specification.GetExpression());
    }

    // With eager loading
    public async Task<List<T>> FindAsync(
        Specification<T> specification,
        params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;
        
        foreach (var include in includes)
        {
            query = query.Include(include);
        }
        
        return await query
            .Where(specification.GetExpression())
            .ToListAsync();
    }
}

Register in dependency injection:

// Program.cs or Startup.cs
services.AddScoped<SpecificationRepository<AppDbContext, Product>>();

Usage with EF Core

public class ProductController : ControllerBase
{
    private readonly SpecificationRepository<AppDbContext, Product> _repository;

    // Find premium products
    [HttpGet("premium")]
    public async Task<IActionResult> GetPremium()
    {
        var spec = new PremiumProductSpecification();
        var products = await _repository.FindAsync(spec);
        return Ok(products);
    }

    // Count in-stock products
    [HttpGet("in-stock/count")]
    public async Task<IActionResult> CountInStock([FromQuery] int minStock = 10)
    {
        var spec = new InStockSpecification(minStock);
        var count = await _repository.CountAsync(spec);
        return Ok(new { count });
    }

    // Check if business products exist
    [HttpGet("business/exists")]
    public async Task<IActionResult> BusinessProductsExist()
    {
        var spec = new BusinessProductSpecification();
        var exists = await _repository.AnyAsync(spec);
        return Ok(new { exists });
    }
}

The repository generates optimized SQL queries:

// For CountAsync with InStockSpecification(10)
SELECT COUNT(*) FROM Products WHERE Stock >= 10

// For FindAsync with PremiumProductSpecification
SELECT * FROM Products 
WHERE Price >= 100 
  AND Stock > 5 
  AND Category IN ('Electronics', 'Office', 'Industrial')

Handling Related Entities

Specifications can work across relationships:


public class ActiveSupplierSpecification : Specification<Product>
{
    protected override Expression<Func<Product, bool>> ToExpression()
    {
        return product => 
            product.Supplier != null &&
            product.Supplier.Status == SupplierStatus.Active &&
            product.Supplier.ReliabilityScore >= 8;
    }
}

// Usage with eager loading
var spec = new ActiveSupplierSpecification();
var products = await _repository.FindAsync(
    spec,
    p => p.Supplier  // Include related supplier
);

Composing Specifications with Logical Operators

One of the most powerful features of the Specification Pattern is composition - combining simple specifications into complex business rules using logical operators.

Logical Operators

We need three fundamental operations:

  • AND: Both specifications must be satisfied
  • OR: At least one specification must be satisfied
  • NOT: Negate a specification

NOT Specification


internal sealed class NotSpecification<T> : Specification<T> where T : class
{
    private readonly Specification<T> _specification;

    public NotSpecification(Specification<T> specification)
    {
        _specification = specification;
    }

    protected override Expression<Func<T, bool>> ToExpression()
    {
        var expression = _specification.GetExpression();
        var parameter = expression.Parameters[0];
        var body = Expression.Not(expression.Body);
        
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }
}

AND Specification


internal sealed class AndSpecification<T> : Specification<T> where T : class
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;

    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }

    protected override Expression<Func<T, bool>> ToExpression()
    {
        var leftExpression = _left.GetExpression();
        var rightExpression = _right.GetExpression();
        
        var parameter = leftExpression.Parameters[0];
        
        // Replace parameter in right expression to match left
        var rightBody = ParameterReplacer.Replace(
            rightExpression.Body,
            rightExpression.Parameters[0],
            parameter
        );
        
        // Combine with AndAlso for short-circuit evaluation
        var body = Expression.AndAlso(leftExpression.Body, rightBody);
        
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }
}

OR Specification


internal sealed class OrSpecification<T> : Specification<T> where T : class
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;

    public OrSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }

    protected override Expression<Func<T, bool>> ToExpression()
    {
        var leftExpression = _left.GetExpression();
        var rightExpression = _right.GetExpression();
        
        var parameter = leftExpression.Parameters[0];
        
        var rightBody = ParameterReplacer.Replace(
            rightExpression.Body,
            rightExpression.Parameters[0],
            parameter
        );
        
        // Combine with OrElse for short-circuit evaluation
        var body = Expression.OrElse(leftExpression.Body, rightBody);
        
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }
}

Parameter Replacer Helper

This helper ensures consistent parameter naming across composed expressions:

internal class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _oldParameter;
    private readonly ParameterExpression _newParameter;

    private ParameterReplacer(
        ParameterExpression oldParameter,
        ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    public static Expression Replace(
        Expression expression,
        ParameterExpression oldParameter,
        ParameterExpression newParameter)
    {
        return new ParameterReplacer(oldParameter, newParameter)
            .Visit(expression);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == _oldParameter ? _newParameter : node;
    }
}

Composition in Action

// Complex business rule: Premium AND in-stock OR clearance, but NOT business-only
var marketingSpec = new PremiumProductSpecification()
    .And(new InStockSpecification(10))
    .Or(new ClearanceSaleSpecification())
    .And(new BusinessProductSpecification().Not());

// Usage
var products = await _repository.FindAsync(marketingSpec);

// Generated SQL (example):
// SELECT * FROM Products 
// WHERE (
//     (Price >= 100 AND Stock > 5 AND Category IN (...) AND Stock >= 10)
//     OR
//     (Price < 50 AND Stock < 10)
// )
// AND NOT (IsBusinessProduct = 1 AND MinimumOrderQuantity >= 10)

Key Benefits:

  • Fluent, readable syntax
  • Type-safe composition
  • Automatically generates optimized SQL
  • Reusable building blocks

Identity Specification for Dynamic Queries

The Identity Specification acts as a neutral element for AND operations, perfect for building dynamic queries with optional filters.

Identity Specification Implementation

internal sealed class IdentitySpecification<T> : Specification<T> where T : class
{
    protected override Expression<Func<T, bool>> ToExpression()
    {
        // Always returns true - matches all entities
        return entity => true;
    }
}

Optimizations in Base Class

public abstract class Specification<T> where T : class
{
    // Identity specification for starting dynamic queries
    public static Specification<T> All => new IdentitySpecification<T>();

    public Specification<T> And(Specification<T> other)
    {
        // Optimization: If left is identity, return right
        if (this is IdentitySpecification<T>)
            return other;
            
        // Optimization: If right is identity, return left
        if (other is IdentitySpecification<T>)
            return this;
            
        return new AndSpecification<T>(this, other);
    }

    public Specification<T> Or(Specification<T> other)
    {
        // Optimization: If either is identity, entire OR is always true
        if (this is IdentitySpecification<T> || other is IdentitySpecification<T>)
            return All;
            
        return new OrSpecification<T>(this, other);
    }
}

Dynamic Search Endpoint

Build queries dynamically based on optional filter parameters:

[HttpGet("search")]
public async Task<IActionResult> Search(
    [FromQuery] string? category = null,
    [FromQuery] int? minStock = null,
    [FromQuery] decimal? minPrice = null)
{
    // Start with identity (match all)
    var spec = Specification<Product>.All;
    
    // Add filters only if provided
    if (!string.IsNullOrWhiteSpace(category))
    {
        spec = spec.And(new CategorySpecification(category));
    }
    
    if (minStock.HasValue)
    {
        spec = spec.And(new InStockSpecification(minStock.Value));
    }
    
    if (minPrice.HasValue)
    {
        spec = spec.And(new MinimumPriceSpecification(minPrice.Value));
    }
    
    var products = await _repository.FindAsync(spec);
    return Ok(products);
}

Query Scenarios:

// No filters - returns all products
GET /search
// SQL: SELECT * FROM Products

// Only category filter
GET /search?category=Electronics
// SQL: SELECT * FROM Products WHERE Category = 'Electronics'

// Multiple filters
GET /search?category=Electronics&minStock=10&minPrice=50
// SQL: SELECT * FROM Products 
//      WHERE Category = 'Electronics' 
//        AND Stock >= 10 
//        AND Price >= 50

The beauty of this approach:

  • No null checks needed
  • Clean, linear code
  • Efficient SQL generation
  • Easy to extend with new filters

Best Practices

1. Structure: Strongly-Typed Classes

Do: Create dedicated specification classes for each business concept

public class PremiumProductSpecification : Specification<Product>
{
    // Clear business meaning
    protected override Expression<Func<Product, bool>> ToExpression() { }
}

Don't: Use generic specifications with external configuration

// Loses business meaning
var spec = new GenericSpecification<Product>(p => p.Price > 100);

2. Focus: Single Responsibility

Keep specifications focused on one business concept:

// ✅ Good - clear purpose
public class ClearanceSaleSpecification : Specification<Product> { }

// ❌ Bad - too flexible
public class ConfigurableProductSpecification : Specification<Product>
{
    public ConfigurableProductSpecification(
        decimal? minPrice, 
        decimal? maxPrice, 
        int? minStock,
        string[] categories) { }
}

3. Immutability: Thread-Safe Specifications

public class InStockSpecification : Specification<Product>
{
    private readonly int _minimumStock;  // ✅ readonly field

    public InStockSpecification(int minimumStock)
    {
        _minimumStock = minimumStock;
    }
    
    // Immutable - safe for concurrent use
}

4. Performance: Monitor Generated SQL

Always verify that specifications generate efficient queries:

// Enable EF Core logging
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information));

// Check generated SQL
var spec = new PremiumProductSpecification();
var products = await _repository.FindAsync(spec);

// Console output:
// SELECT * FROM Products 
// WHERE Price >= 100 AND Stock > 5 AND ...

5. Repository: Specification-Aware Methods

// ✅ Good - uses specifications
public Task<List<T>> FindAsync(Specification<T> spec) { }
public Task<int> CountAsync(Specification<T> spec) { }

// ❌ Bad - specific methods for each rule
public Task<List<Product>> GetPremiumProducts() { }
public Task<List<Product>> GetPremiumInStockProducts() { }
public Task<List<Product>> GetPremiumBusinessProducts() { }
// ... hundreds more

6. Composition: Use Logical Operators

Prefer composition over inheritance:

// ✅ Good - composable
var spec = new PremiumProductSpecification()
    .And(new InStockSpecification(10))
    .Or(new ClearanceSaleSpecification());

// ❌ Bad - inheritance hierarchy
public class PremiumInStockSpecification : PremiumProductSpecification { }
public class PremiumBusinessSpecification : PremiumProductSpecification { }

7. Testing: Independent Verification

[Fact]
public void PremiumProductSpecification_Should_Match_Qualified_Products()
{
    // Arrange
    var spec = new PremiumProductSpecification();
    var qualifiedProduct = new Product 
    { 
        Price = 150, 
        Stock = 10, 
        Category = "Electronics" 
    };
    var unqualifiedProduct = new Product 
    { 
        Price = 50, 
        Stock = 10, 
        Category = "Electronics" 
    };

    // Act & Assert
    Assert.True(spec.IsSatisfiedBy(qualifiedProduct));
    Assert.False(spec.IsSatisfiedBy(unqualifiedProduct));
}

When to Use Specifications

Three key questions to guide your decision:

Question Yes = Use Specifications No = Simple Code
Does logic appear in multiple places? Specifications eliminate duplication Direct implementation is fine
Is this an enterprise application? Specifications provide structure Simple solutions work well
Will business rules change frequently? Single source of truth Scattered logic acceptable

Perfect for:

  • Enterprise applications with complex business rules
  • E-commerce platforms with product filtering and eligibility
  • Financial systems with regulatory compliance rules
  • Multi-team codebases needing consistency

Not recommended for:

  • Simple CRUD applications
  • One-time data migrations
  • Prototype or proof-of-concept projects
  • Ultra-high-performance systems where microseconds matter

Summary

In this article, we explored how the Specification Pattern from Domain-Driven Design eliminates scattered and duplicated business logic in enterprise applications.

Key Takeaways:

  • Problem Solved: Scattered business logic leads to duplication, inconsistency, and maintenance nightmares. The Specification Pattern provides a single source of truth.
  • Core Concept: Specifications encapsulate business rules into reusable objects that work for both database queries and in-memory validation.
  • Expression Trees: Understanding the difference between delegates and expression trees is crucial. Entity Framework Core needs expression trees to generate SQL.
  • Base Class: A robust base specification class handles compilation caching, null checks, and provides a clean API for consumers.
  • Strongly-Typed: Each business concept gets its own specification class, maintaining clear business meaning.
  • Composition: Logical operators (AND, OR, NOT) enable building complex rules from simple specifications.
  • Identity Pattern: The identity specification enables clean dynamic query building with optional filters.
  • Best Practices: Keep specifications focused, immutable, and testable. Monitor generated SQL for performance.

The Specification Pattern transforms maintenance nightmares into elegant, maintainable code. When business requirements change, you update exactly one place, and the change propagates automatically to all consumers. This is the power of proper domain-driven design.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • DDD
  • Specification Pattern
  • Domain-Driven Design
  • DDD
  • Business Logic
  • Entity Framework Core
  • Expression Trees
  • LINQ
  • Design Patterns