Specification.Net

I was introduced to the Specification pattern after seeing a small implementation shared online. The idea seemed simple at first, but the more I thought about it, the more I liked it. It is one of those patterns that feels almost too obvious once you see it, but it solves a very real problem: business rules tend to get duplicated and spread across the codebase.

What makes the pattern interesting to me is that it takes a rule that would normally live inside a method and turns it into a named, reusable object. Instead of scattering checks through services and repositories, we get to express the rule once and reuse it. That makes the logic easier to read, easier to test, and easier to reason about.

The Specification Pattern

A specification is basically a predicate: given an entity, it returns true or false based on a business rule. In other words, a specification says, "this object satisfies this rule." That seems small, but it is surprisingly powerful when the rules become more complex.

To make it concrete, let's talk about an inventory system. We have items that can be stocked and sold. The rules are simple enough to understand:

Without the pattern, those checks tend to show up in multiple places. A service method might validate stock, another might check the item, and then the repository logic adds another layer. The same rules start to drift and repeat. That is where the Specification pattern starts to pay off.

public record Item(string Name, string Sku, decimal Price);

public class StockedItem
{
    public Item Item { get; }
    public int Stock { get; private set; }

    public StockedItem(Item item, int stock)
    {
        Item = item;
        Stock = stock;
    }
}

public class Inventory
{
    public List<StockedItem> StockedItems { get; } = new();

    public bool Stock(Item item, int amount)
    {
        if (amount > 0)
        {
            var stockedItem = StockedItems.FirstOrDefault(i => i.Item == item);
            if (stockedItem != null)
            {
                stockedItem.Stock += amount;
                return true;
            }
        }

        return false;
    }

    public bool Sell(Item item, int amount)
    {
        if (amount > 0)
        {
            var stockedItem = StockedItems.FirstOrDefault(i => i.Item == item);
            if (stockedItem != null && stockedItem.Stock >= amount)
            {
                stockedItem.Stock -= amount;
                return true;
            }
        }

        return false;
    }
}

This works. It also has a problem. The same kinds of rules are embedded in both methods. The logic is not terrible, but it is duplicated and hard to reuse elsewhere. If we want to check whether an item is valid in a different place, we have to reimplement that logic. That is the kind of thing I like to pull out into a dedicated abstraction.

That is the idea behind the pattern.

A Simple Specification

public abstract class Specification<T>
{
    private readonly Func<T, bool> _compiledSpec;

    public Expression<Func<T, bool>> Expression { get; }

    protected Specification(Expression<Func<T, bool>> spec)
    {
        Expression = spec;
        _compiledSpec = spec.Compile();
    }

    public bool IsSatisfiedBy(T item) => _compiledSpec(item);
}

public class MatchingStockItem : Specification<StockedItem>
{
    public MatchingStockItem(Item item)
        : base(si => si.Item == item)
    {
    }
}

public class HasEnoughStock : Specification<StockedItem>
{
    public HasEnoughStock(int amount)
        : base(si => si.Stock >= amount)
    {
    }
}

Now we have two small rules. One checks whether a stocked item matches the item we want. The other checks whether it has enough stock. These are each simple, focused pieces of business logic. We can test them independently and reuse them in multiple places.

public class Inventory
{
    public List<StockedItem> StockedItems { get; } = new();

    public bool Stock(Item item, int amount)
    {
        if (amount <= 0)
        {
            return false;
        }

        var stockedItem = StockedItems
            .FirstOrDefault(new MatchingStockItem(item).IsSatisfiedBy);

        if (stockedItem is null)
        {
            return false;
        }

        stockedItem.Stock += amount;
        return true;
    }

    public bool Sell(Item item, int amount)
    {
        if (amount <= 0)
        {
            return false;
        }

        var stockedItem = StockedItems
            .FirstOrDefault(new MatchingStockItem(item).IsSatisfiedBy);

        if (stockedItem is null)
        {
            return false;
        }

        if (!new HasEnoughStock(amount).IsSatisfiedBy(stockedItem))
        {
            return false;
        }

        stockedItem.Stock -= amount;
        return true;
    }
}

At this point, it is still a little more ceremony than the original code. I do not think anyone would say this is a dramatic simplification yet. But the important thing is that the rules are named, and they are now reusable. We are starting to get value out of the abstraction.

Composable Specifications

This is where the pattern really starts to feel useful. These small rules can be combined into larger ones, which gives us a way to describe business logic in a clean and expressive way.

For example, we might want a rule that says, "the item matches the requested item and has enough stock." That is just a composition of two smaller specifications. We can build that with And, Or, and Not operations.

using LinqExpression = System.Linq.Expressions.Expression;

public abstract class Specification<T>
{
    public Expression<Func<T, bool>> Expression { get; }
    private readonly Func<T, bool> _compiledSpec;

    protected Specification(Expression<Func<T, bool>> expression)
    {
        Expression = expression;
        _compiledSpec = expression.Compile();
    }

    public bool IsSatisfiedBy(T item) => _compiledSpec(item);
}

public class AndSpecification<T> : Specification<T>
{
    public AndSpecification(Specification<T> left, Specification<T> right)
        : base(
            LinqExpression.Lambda<Func<T, bool>>(
                LinqExpression.AndAlso(
                    left.Expression.Body,
                    left.Expression.Parameters[0] == right.Expression.Parameters[0]
                        ? right.Expression.Body
                        : LinqExpression.Invoke(right.Expression, left.Expression.Parameters[0])),
                left.Expression.Parameters[0]))
    {
    }
}

public class OrSpecification<T> : Specification<T>
{
    public OrSpecification(Specification<T> left, Specification<T> right)
        : base(
            LinqExpression.Lambda<Func<T, bool>>(
                LinqExpression.OrElse(
                    left.Expression.Body,
                    left.Expression.Parameters[0] == right.Expression.Parameters[0]
                        ? right.Expression.Body
                        : LinqExpression.Invoke(right.Expression, left.Expression.Parameters[0])),
                left.Expression.Parameters[0]))
    {
    }
}

public static class SpecificationExtensions
{
    public static Specification<T> And<T>(this Specification<T> left, Specification<T> right)
        => new AndSpecification<T>(left, right);

    public static Specification<T> Or<T>(this Specification<T> left, Specification<T> right)
        => new OrSpecification<T>(left, right);
}

Now our rules can be composed into larger decisions without duplicating the logic:

var canSell = new MatchingStockItem(item)
    .And(new HasEnoughStock(amount));

if (canSell.IsSatisfiedBy(stockedItem))
{
    stockedItem.Stock -= amount;
}

That is the real value for me. We are no longer writing one-off checks in every method. We are describing business rules as small units and combining them when needed. That tends to make the code easier to follow, easier to extend, and easier to test.

Entity Framework Integration

The pattern becomes even more compelling when we integrate it with Entity Framework Core. EF Core uses expression trees for queries, and our specification objects already carry expressions. That means the same rules we use in memory can often be pushed down into the database as a query.

That is a nice benefit. Instead of loading a bunch of data into memory and applying the checks after the fact, we can let the database do the filtering for us. It keeps the query logic close to the source of truth and avoids unnecessary work.

public class StockedItem
{
    public Item Item { get; private set; }
    public int Stock { get; private set; }

    public void AddStock(int amount)
    {
        Stock += amount;
    }

    public void RemoveStock(int amount)
    {
        Stock -= amount;
    }
}

public class MatchingStockItem : Specification<StockedItem>
{
    public MatchingStockItem(string sku)
        : base(si => si.Item.Sku == sku)
    {
    }
}

public class HasEnoughStock : Specification<StockedItem>
{
    public HasEnoughStock(int amount)
        : base(si => si.Stock >= amount)
    {
    }
}

public class StockHandler
{
    private readonly DataContext _context;

    public StockHandler(DataContext context)
    {
        _context = context;
    }

    public async Task<bool> Stock(string sku, int amount)
    {
        if (amount <= 0)
        {
            return false;
        }

        var stockedItem = await _context.StockedItems
            .FirstOrDefaultAsync(new MatchingStockItem(sku).Expression);

        if (stockedItem is null)
        {
            return false;
        }

        stockedItem.AddStock(amount);
        return true;
    }

    public async Task<bool> Sell(string sku, int amount)
    {
        if (amount <= 0)
        {
            return false;
        }

        var stockedItem = await _context.StockedItems
            .FirstOrDefaultAsync(
                new MatchingStockItem(sku)
                    .And(new HasEnoughStock(amount))
                    .Expression);

        if (stockedItem is null)
        {
            return false;
        }

        stockedItem.RemoveStock(amount);
        return true;
    }
}

The main thing to remember here is that you need to use the expression itself when passing the specification to EF Core. If you pass the object directly, it is not going to be able to translate it into a query. That is the one bit of friction you need to watch for.

That said, this pattern pairs very nicely with the Repository pattern. A repository can accept a specification, translate it into a database query, and still leave the rule itself easy to test and reason about in isolation. That is a really nice separation of concerns.

public interface IReadOnlyRepository<T>
{
    Task<T?> Get(Specification<T> spec);
    Task<IEnumerable<T>> GetAll(Specification<T> spec);
}

public interface IRepository<T>
{
    Task Create(T item);
    Task Update(T item);
    Task Delete(T item);
    Task Delete(Specification<T> spec);
}

public class InventoryItemRepository : IReadOnlyRepository<StockedItem>, IRepository<StockedItem>
{
    private readonly DataContext _context;

    public InventoryItemRepository(DataContext context)
    {
        _context = context;
    }

    public async Task<StockedItem?> Get(Specification<StockedItem> spec)
        => await _context.InventoryItems.FirstOrDefaultAsync(spec.Expression);

    public async Task<IEnumerable<StockedItem>> GetAll(Specification<StockedItem> spec)
        => await _context.InventoryItems.Where(spec.Expression).ToArrayAsync();

    public Task Create(StockedItem item) => _context.InventoryItems.AddAsync(item).AsTask();
    public Task Update(StockedItem item) => Task.CompletedTask;
    public Task Delete(StockedItem item) => Task.CompletedTask;
    public Task Delete(Specification<StockedItem> spec) => _context.InventoryItems.Where(spec.Expression).ExecuteDeleteAsync();
}

At that point, the pattern is doing exactly what I like to see from a design idea: it is reusable, testable, and cheap to apply. We get the business rules in one place, the query logic in another place, and the code reads more like the domain than a pile of conditional expressions.

Conclusion

The Specification pattern is not magic, and it is not always the best fit. It is not a replacement for every validation rule or every repository query. But when the business rules start to pile up, it gives us a clean way to express them without scattering them across the application.

I like it because it keeps the domain language in the code. Instead of asking, "Where is that rule implemented again?" we can point at a specification and read what it means. That makes the logic easier to maintain and easier to understand.

That, to me, is the real win.