Advanced

Specification Pattern in C-Sharp

Domain-Driven Design (DDD) brings together many established patterns. The Specification Pattern is one of them. It helps gather domain knowledge in one place and then reuse it to perform...

Level: Intermediate


Table of Contents

  1. Course presentation
  2. Introduction — Theory and example project
  1. Naive implementation of the Specification Pattern
  1. Refactoring towards better encapsulation
  1. Resources and Conclusion
  2. Complete source code reference

1. Course presentation

This course is an in-depth description of the implementation of the Specification Pattern in C#. Vladimir Khorikov, back-end developer, guides the developer through all the concepts and practical considerations related to this pattern from Domain-Driven Design (DDD).

Main topics covered

  • The use cases of the Specification Pattern
  • Common anti-patterns in this area
  • Integration with ORMs (NHibernate / Entity Framework)
  • Maintaining correct encapsulation

Prerequisites

  • Mastery of the C# language
  • Basic notions of LINQ and object-oriented programming

2. Introduction — Theory and example project

Domain-Driven Design (DDD) brings together many established patterns. The Specification Pattern is one of them. It helps gather domain knowledge in one place and then reuse it to perform database searches, in-memory validations, and creation of new objects.

Despite the fact that this pattern is well known, developers often struggle to implement it correctly in a modern context, especially in combination with ORMs.


2.1 What is the Specification Pattern?

The Specification Pattern is a pattern that helps achieve two goals:

  1. Avoid duplication of domain knowledge in certain scenarios — in other words, follow the DRY (Don’t Repeat Yourself) principle.
  2. Enable a declarative approach, which increases the maintainability of the code.

The pattern was introduced by Eric Evans and Martin Fowler in the early 2000s in a joint article. Eric Evans later referred to it in his book Domain-Driven Design.

The basic idea: when you have a piece of domain knowledge, you can encapsulate that knowledge into a single unit — the specification — and reuse it in different parts of the code.


2.2 Use cases

There are three use cases for the Specification Pattern:

#Use casesDescription
1Memory validationCheck if an available item meets certain criteria. Useful when validating incoming requests from users or third-party systems.
2Retrieving data from databaseFind records that match the specification.
3Creation of new objectsCreate objects that meet the criteria. Useful when you don’t care about the exact contents of objects, but need them to have certain attributes.

These three use cases don’t have much in common at first glance. The usual approach is to duplicate logic for each, which increases technical debt. This is precisely what the Specification Pattern solves.

The two most important use cases in practice are:

  • validation in memory
  • retrieving data from the database

2.3 Presentation of the example project

The project used throughout the course is a film library: a catalog where you can search for films and buy tickets. The user interface is implemented with WPF, but the UI technology is secondary — the concepts apply to any C# application working with a relational database (desktop application or Web API backend).

Project structure

  • UI: WPF — MovieListViewModel handles user interactions via commands
  • Logic: Business layer — entities, repository, mappings
  • Database: SQL Server with NHibernate as ORM

Initial Movie entity

// Module 2 — État initial
public class Movie : Entity
{
    public virtual string Name { get; }
    public virtual DateTime ReleaseDate { get; }
    public virtual MpaaRating MpaaRating { get; }
    public virtual string Genre { get; }
    public virtual double Rating { get; }

    protected Movie()
    {
    }
}

public enum MpaaRating
{
    G = 1,
    PG = 2,
    PG13 = 3,
    R = 4
}

Initial Repository

// Module 2 — Repository sans filtrage
public class MovieRepository
{
    public Maybe<Movie> GetOne(long id)
    {
        using (ISession session = SessionFactory.OpenSession())
        {
            return session.Get<Movie>(id);
        }
    }

    public IReadOnlyList<Movie> GetList()
    {
        using (ISession session = SessionFactory.OpenSession())
        {
            return session.Query<Movie>().ToList();
        }
    }
}

Database creation SQL script

CREATE DATABASE [SpecPattern]
GO
USE [SpecPattern]
GO

CREATE TABLE [dbo].[Movie](
    [MovieID]     [bigint] IDENTITY(1,1) NOT NULL,
    [Name]        [nvarchar](50) NOT NULL,
    [ReleaseDate] [datetime] NOT NULL,
    [MpaaRating]  [int] NOT NULL,
    [Genre]       [varchar](50) NOT NULL,
    [Rating]      [float] NOT NULL,
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([MovieID] ASC)
)

INSERT [dbo].[Movie] VALUES (1, N'The Amazing Spider-Man', '2012-07-03', 3, N'Adventure', 7)
INSERT [dbo].[Movie] VALUES (2, N'Beauty and the Beast',  '2017-03-17', 3, N'Family',    7.8)
INSERT [dbo].[Movie] VALUES (3, N'The Secret Life of Pets','2016-07-08', 1, N'Adventure', 6.6)
INSERT [dbo].[Movie] VALUES (4, N'The Jungle Book',       '2016-04-15', 2, N'Fantasy',   7.5)
INSERT [dbo].[Movie] VALUES (5, N'Split',                 '2017-01-20', 3, N'Horror',    7.4)
INSERT [dbo].[Movie] VALUES (6, N'The Mummy',             '2017-06-09', 4, N'Action',    6.7)

2.4 Added new search options

A new feature is requested: implement a filtered search. The user should be able to filter films by:

  1. Kids’ Movies (ForKidsOnly): only show movies with an MpaaRating of G or PG
  2. Minimum Rating (MinimumRating): filter by rating
  3. Available on CD (OnCD): films released more than 6 months

Problem: Business rule is duplicated in the repository

// Module 2 — Repository avec filtrage "brut"
public IReadOnlyList<Movie> GetList(
    bool forKidsOnly,
    double minimumRating,
    bool availableOnCD)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<Movie>()
            .Where(x =>
                (x.MpaaRating <= MpaaRating.PG || !forKidsOnly) &&
                x.Rating >= minimumRating &&
                (x.ReleaseDate <= DateTime.Now.AddMonths(-6) || !availableOnCD))
            .ToList();
    }
}

Note: Although MpaaRating is an enum, we can use the comparison operator <= because its underlying database value is an integer. The ORM transforms the PG value into an integer when translating to SQL.


2.5 Added new purchase options

New functional requests appear: in addition to the standard adult ticket, the user must be able to purchase:

  1. A child’s ticket — only for G or PG films
  2. A CD — only for films released more than 6 months ago

These new features require in-memory validation: unlike search (which filters in base), here we must validate that the selected film satisfies the criterion.

ViewModel with logic duplication

// Module 2 — ViewModel avant refactoring
private void BuyCD(long movieId)
{
    Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
    if (movieOrNothing.HasNoValue)
        return;

    Movie movie = movieOrNothing.Value;

    // DUPLICATION : même règle que dans le repository
    if (movie.ReleaseDate >= DateTime.Now.AddMonths(-6))
    {
        MessageBox.Show("The movie doesn't have a CD version", "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
        return;
    }

    MessageBox.Show("You've bought a ticket", "Success",
        MessageBoxButton.OK, MessageBoxImage.Information);
}

private void BuyChildTicket(long movieId)
{
    Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
    if (movieOrNothing.HasNoValue)
        return;

    Movie movie = movieOrNothing.Value;

    // DUPLICATION : même règle que dans le repository
    if (!movie.IsSuitableForChildren())
    {
        MessageBox.Show("The movie is not suitable for children", "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
        return;
    }

    MessageBox.Show("You've bought a ticket", "Success",
        MessageBoxButton.OK, MessageBoxImage.Information);
}

2.6 Code review and identified issues

At this point, two categories of use cases exist:

  • Retrieving data from the database (repository)
  • Validating incoming requests (view model)

Although these cases are different, they share the same business knowledge:

  • What makes a film suitable for children?
  • Which films have a CD version?

The problem: scattered domain knowledge

Repository.GetList()  →  x.MpaaRating <= MpaaRating.PG
ViewModel.BuyChildTicket()  →  movie.IsSuitableForChildren()

Knowledge is duplicated in violation of the DRY principle.

Attempted solution: methods on the Movie entity

// Tentative d'encapsulation dans l'entité — problème ORM
public class Movie : Entity
{
    // ...

    public virtual bool IsSuitableForChildren()
    {
        return MpaaRating <= MpaaRating.PG;
    }

    public virtual bool HasCDVersion()
    {
        return ReleaseDate <= DateTime.Now.AddMonths(-6);
    }
}

Using these methods in the repository:

// Cela ne fonctionne PAS avec l'ORM !
session.Query<Movie>()
    .Where(x => x.IsSuitableForChildren())  // Exception à l'exécution

Why does this fail? The ORM is trying to convert the LINQ expression to SQL. It can understand simple predicates (==, <=, etc.), but cannot translate custom C# methods to SQL — there is no native SQL function for IsSuitableForChildren().

One could work around this by loading all the movies into memory and then filtering, but this would be extremely inefficient for large databases.


2.7 Module 2 Summary

  • The Specification Pattern makes it possible to encapsulate domain knowledge in a single unit and reuse it in different scenarios.
  • The three use cases: in-memory validation, data recovery, object creation.
  • Without this pattern, you end up duplicating business logic or writing inefficient SQL queries.

3. Naive implementation of the Specification Pattern

In this module, we explore two naive approaches to solving the duplication problem: raw C# expressions and Generic Specifications. We see why they are insufficient.


3.1 How LINQ works

To understand the correct implementation, one must first understand how LINQ works in C#.

The two fundamental interfaces

InterfaceUsagePredicate type
IEnumerable<T>Manipulating objects in memoryFunc<T, bool> (delegate)
IQueryable<T>Generation of SQL queriesExpression<Func<T, bool>>

Despite identical names (e.g. Where), the two versions behave totally differently.

// Avec IEnumerable — le prédicat est un DELEGATE
int[] array = { 1, 2, 3 };
array.Where(x => x > 1);  // Where(Func<int, bool>)

// Avec IQueryable — le prédicat est une EXPRESSION
IQueryable<Movie> movies = session.Query<Movie>();
movies.Where(x => x.Rating > 7);  // Where(Expression<Func<Movie, bool>>)

Delegates vs Expressions

DelegatePhrase
Compiled inExecutable IL codeSyntax tree (AST)
ExecutionBy the processorParsed at runtime, translated into SQL
ConversionDelegate → Expression: impossibleExpression → Delegate: possible via .Compile()
// Un lambda peut être compilé comme delegate ou comme expression
Func<int, bool> predicate = x => x > 1;             // Delegate
Expression<Func<int, bool>> expr = x => x > 1;      // Expression

Key to understanding: The Expression represents a code tree (AST) which can be explored at runtime. This is what the ORM uses to generate the SQL. We can compile an Expression into Func (delegate), but the reverse is impossible.


3.2 Using simple C# expressions

The natural solution is to define expressions in the Movie class and reuse them both in the repository (as an expression for the ORM) and in the view model (after compilation in delegate).

// Module 3 — Movie avec expressions statiques
public class Movie : Entity
{
    public virtual string Name { get; }
    public virtual DateTime ReleaseDate { get; }
    public virtual MpaaRating MpaaRating { get; }
    public virtual string Genre { get; }
    public virtual double Rating { get; }

    protected Movie() { }

    // Expression réutilisable
    public static readonly Expression<Func<Movie, bool>> IsSuitableForChildren
        = movie => movie.MpaaRating <= MpaaRating.PG;

    public static readonly Expression<Func<Movie, bool>> HasCDVersion
        = movie => movie.ReleaseDate <= DateTime.Now.AddMonths(-6);
}

Use in the repository (SQL query)

// L'expression est passée directement à Where() → traduite en SQL
public IReadOnlyList<Movie> GetList(Expression<Func<Movie, bool>> specification)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<Movie>()
            .Where(specification)
            .ToList();
    }
}

Use in ViewModel (in-memory validation)

// Compilation de l'expression en delegate pour usage en mémoire
private void BuyChildTicket(long movieId)
{
    Movie movie = _repository.GetOne(movieId).Value;

    // Compilation puis invocation
    Func<Movie, bool> predicate = Movie.IsSuitableForChildren.Compile();
    if (!predicate(movie))
    {
        MessageBox.Show("The movie is not suitable for children");
        return;
    }
    MessageBox.Show("You've bought a ticket");
}
private void Search()
{
    // Expression "fourre-tout" (toujours vraie par défaut)
    Expression<Func<Movie, bool>> criteria = movie => true;

    if (ForKidsOnly)
        criteria = Movie.IsSuitableForChildren;

    Movies = _repository.GetList(criteria);
    Notify(nameof(Movies));
}

3.3 Limits of simple C# expressions

Although this approach eliminates duplication, it has two major drawbacks:

Disadvantage 1: Expressions do not combine easily

// IMPOSSIBLE directement — ne compile pas
var combined = Movie.IsSuitableForChildren && Movie.HasCDVersion;

To combine two C# expressions, you must manually deconstruct the two expressions and create a new one — a complex operation that should not be the responsibility of the client code.

Disadvantage 2: Client code must handle low-level details

// Le code client doit appeler .Compile() lui-même → détail d'implémentation exposé
Func<Movie, bool> predicate = Movie.IsSuitableForChildren.Compile();
bool result = predicate(movie);

Ideally, client code should only call a single method on an object and get the validation result — without worrying about compiling expressions.

Conclusion: Raw C# expressions lack encapsulation. We need to raise the level of abstraction.


3.4 Generic Specifications

To solve the encapsulation problem, we introduce a generic specification class:

// Generic Specification — enveloppe une expression quelconque
public class GenericSpecification<T>
{
    public Expression<Func<T, bool>> Expression { get; }

    public GenericSpecification(Expression<Func<T, bool>> expression)
    {
        Expression = expression;
    }

    public bool IsSatisfiedBy(T entity)
    {
        return Expression.Compile().Invoke(entity);
    }
}

Use in ViewModel

private void BuyChildTicket(long movieId)
{
    Movie movie = _repository.GetOne(movieId).Value;

    // Plus besoin d'appeler .Compile() manuellement
    var spec = new GenericSpecification<Movie>(Movie.IsSuitableForChildren);
    if (!spec.IsSatisfiedBy(movie))
    {
        MessageBox.Show("The movie is not suitable for children");
        return;
    }
    MessageBox.Show("You've bought a ticket");
}

Use in the Repository

// Le repository accepte maintenant une GenericSpecification
public IReadOnlyList<Movie> GetList(GenericSpecification<Movie> specification)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<Movie>()
            .Where(specification.Expression)  // .Expression pour l'ORM
            .ToList();
    }
}

3.5 Limitations of Generic Specifications

The GenericSpecification contains no domain knowledge. It just accepts an expression from the outside and provides some utility methods on top of it.

GenericSpecification : thin wrapper — aucune connaissance encapsulée
KidsMovieSpecification : strongy-typed — connaissance encapsulée ✓

A Generic Specification is just a thin wrapper around an expression provided by client code. It’s the client code that decides the logic, not the specification.

Anti-pattern: Generic Specifications do not provide real encapsulation. Strongly Typed Specifications must be used.

The right approach:

  • KidsMovieSpecification — encapsulates the knowledge “what is a children’s movie”
  • HasCDVersionSpecification — encapsulates the knowledge “which movie has a CD version”

3.6 Return IQueryable from a Repository — Anti-pattern

Another approach is to return IQueryable<T> from the repository, letting the client code construct its own queries:

// Anti-pattern : retourner IQueryable
public class MovieRepository
{
    public IQueryable<Movie> Find()
    {
        ISession session = SessionFactory.OpenSession();
        return session.Query<Movie>();  // IQueryable retourné directement
    }
}

// Dans le ViewModel — filtrage côté client
private void Search()
{
    IQueryable<Movie> query = _repository.Find();

    if (ForKidsOnly)
        query = query.Where(x => x.MpaaRating <= MpaaRating.PG);

    if (OnCD)
        query = query.Where(x => x.ReleaseDate <= DateTime.Now.AddMonths(-6));

    Movies = query.ToList();
}

Why is this an anti-pattern?

IQueryable<T> allows arbitrary expressions in Where clauses. The ORM cannot translate all C# expressions to SQL — some cause runtime exceptions.

Violation of the Liskov Substitution Principle (LSP): the repository promises functionality that it cannot always deliver.

// Ceci compile mais lève une exception à l'exécution
_repository.Find()
    .Where(x => x.IsSuitableForChildren())  // ← méthode C# non traduisible en SQL
    .ToList();

This may be acceptable in very small projects, but in medium or large projects it breaks encapsulation and makes the code fragile.

Rule: Avoid returning IQueryable from public methods. Likewise, prefer IReadOnlyList<T> to IEnumerable<T> for collections returned by repositories, because IEnumerable can hide unwanted deferred execution.


3.7 Module 3 Summary

  • LINQ relies on two interfaces: IEnumerable (in memory) and IQueryable (SQL via ORM).
  • C# lambdas can be compiled into delegates (normal execution) or into Expressions (syntax tree explored at execution).
  • Raw C# expressions eliminate duplication but expose low-level details to client code.
  • Generic Specifications provide a slight abstraction but contain no domain knowledge.
  • Returning IQueryable from a repository is an anti-pattern because it violates the LSP.
  • The correct solution: Strongly Typed Specifications.

4. Refactoring towards better encapsulation

This module presents the correct implementation of the Specification Pattern: Strongly Typed Specifications with support for combining expressions.


4.1 Strongly Typed Specifications

A Strongly Typed Specification actually contains the domain knowledge, instead of just accepting it from the outside.

Abstract base class Specification<T>

// Fichier : Logic/Movies/Specification.cs

public abstract class Specification<T>
{
    public static readonly Specification<T> All = new IdentitySpecification<T>();

    // Validation en mémoire — compile l'expression en delegate
    public bool IsSatisfiedBy(T entity)
    {
        Func<T, bool> predicate = ToExpression().Compile();
        return predicate(entity);
    }

    // Méthode abstraite — doit être implémentée par les sous-classes
    public abstract Expression<Func<T, bool>> ToExpression();

    // Méthodes de combinaison
    public Specification<T> And(Specification<T> specification) { ... }
    public Specification<T> Or(Specification<T> specification) { ... }
    public Specification<T> Not() { ... }
}

Key difference with GenericSpecification: The new version does not accept an expression from outside — it forces subclasses to provide this expression. This ensures that each specification contains its own domain knowledge.

IdentitySpecification (Null Object Pattern)

// Specification "identité" — retourne toujours true
internal sealed class IdentitySpecification<T> : Specification<T>
{
    public override Expression<Func<T, bool>> ToExpression()
    {
        return x => true;
    }
}

Specification<T>.All is an instance of IdentitySpecification<T>. It is useful for building dynamic queries: we start from an “all” specification and add conditions via And().

MovieForKidsSpecification — Strongly Typed

// Connaissance du domaine encapsulée dans la classe elle-même
public sealed class MovieForKidsSpecification : Specification<Movie>
{
    public override Expression<Func<Movie, bool>> ToExpression()
    {
        return movie => movie.MpaaRating <= MpaaRating.PG;
    }
}

AvailableOnCDSpecification — Strongly Typed

public sealed class AvailableOnCDSpecification : Specification<Movie>
{
    private const int MonthsBeforeDVDIsOut = 6;

    public override Expression<Func<Movie, bool>> ToExpression()
    {
        return movie => movie.ReleaseDate <= DateTime.Now.AddMonths(-MonthsBeforeDVDIsOut);
    }
}

Use in ViewModel — in-memory validation

private void BuyChildTicket(long movieId)
{
    Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
    if (movieOrNothing.HasNoValue)
        return;

    Movie movie = movieOrNothing.Value;

    // Code client simple : instanciation + IsSatisfiedBy
    var spec = new MovieForKidsSpecification();
    if (!spec.IsSatisfiedBy(movie))
    {
        MessageBox.Show("The movie is not suitable for children", "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
        return;
    }

    MessageBox.Show("You've bought a ticket", "Success",
        MessageBoxButton.OK, MessageBoxImage.Information);
}

private void BuyCD(long movieId)
{
    Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
    if (movieOrNothing.HasNoValue)
        return;

    Movie movie = movieOrNothing.Value;
    var spec = new AvailableOnCDSpecification();
    if (!spec.IsSatisfiedBy(movie))
    {
        MessageBox.Show("The movie doesn't have a CD version", "Error",
            MessageBoxButton.OK, MessageBoxImage.Error);
        return;
    }

    MessageBox.Show("You've bought a ticket", "Success",
        MessageBoxButton.OK, MessageBoxImage.Information);
}

Repository with Specification

public IReadOnlyList<MovieDto> GetList(
    Specification<Movie> specification,
    double minimumRating,
    int page = 0,
    int pageSize = 20)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<Movie>()
            .Where(specification.ToExpression())   // Expression vers SQL
            .Where(x => x.Rating >= minimumRating)
            .Skip(page * pageSize)
            .Take(pageSize)
            .Fetch(x => x.Director)
            .ToList()
            .Select(x => new MovieDto
            {
                Name = x.Name,
                Director = x.Director.Name,
                Genre = x.Genre,
                Id = x.Id,
                MpaaRating = x.MpaaRating.ToString(),
                Rating = x.Rating,
                ReleaseDate = x.ReleaseDate
            })
            .ToList();
    }
}

4.2 General guidelines

1. Do not introduce an ISpecification interface

We sometimes see implementations that introduce an ISpecification<T> interface in addition to the abstract base class:

// ❌ Inutile — viole le principe YAGNI
public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
    Expression<Func<T, bool>> ToExpression();
}

This interface does not bring any added value in practice. The abstract class Specification<T> is sufficient. Do not introduce this interface to follow the YAGNI (You Aren’t Gonna Need It) principle — don’t introduce code or functionality that you don’t need now.

2. Make the Specifications as specific as possible

By default, do not introduce variety into specifications unless you really need it.

Example: the constant MonthsBeforeDVDIsOut = 6 is fixed in AvailableOnCDSpecification. We could make it configurable, but if current usage does not require it, it would be useless.

// ✅ Spécifique par défaut
public sealed class AvailableOnCDSpecification : Specification<Movie>
{
    private const int MonthsBeforeDVDIsOut = 6;
    // ...
}

// ❌ Trop générique si non nécessaire
public sealed class AvailableOnCDSpecification : Specification<Movie>
{
    private readonly int _months;
    public AvailableOnCDSpecification(int months = 6) { _months = months; }
    // ...
}

3. Make Specifications immutable

If you want to modify a specification, instantiate a new one rather than modifying the existing one. Immutability makes it easier to reason about code.

4. Sealing classes (sealed)

Where possible, seal specification classes with sealed. This prevents unintended inheritance and clarifies the intent.


4.3 Combination of Specifications

One of the most powerful aspects of the Specification Pattern is the ability to combine simple specifications to create sophisticated queries.

Example of possible combinations

// Films pour enfants disponibles en CD
var spec1 = new MovieForKidsSpecification().And(new AvailableOnCDSpecification());

// Films en CD qui ne sont PAS pour enfants (soirée entre adultes)
var spec2 = new AvailableOnCDSpecification().And(new MovieForKidsSpecification().Not());

// Films pour enfants OU en CD
var spec3 = new MovieForKidsSpecification().Or(new AvailableOnCDSpecification());

The three types of combination

OperationInternal classDescription
AndAndSpecification<T>Both conditions must be true
GoldOrSpecification<T>At least one condition must be true
NotNotSpecification<T>Reverse the condition

Implementation of AndSpecification<T>

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

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

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression  = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();

        // Combinaison des corps d'expressions
        BinaryExpression andExpression = Expression.AndAlso(
            leftExpression.Body,
            rightExpression.Body);

        // Création d'un nouveau lambda avec le paramètre de gauche
        return Expression.Lambda<Func<T, bool>>(
            andExpression,
            leftExpression.Parameters.Single());
    }
}

Implementation of OrSpecification<T>

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

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

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression  = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();

        BinaryExpression orExpression = Expression.OrElse(
            leftExpression.Body,
            rightExpression.Body);

        return Expression.Lambda<Func<T, bool>>(
            orExpression,
            leftExpression.Parameters.Single());
    }
}

Implementation of NotSpecification<T>

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

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

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> expression = _specification.ToExpression();
        UnaryExpression notExpression = Expression.Not(expression.Body);

        return Expression.Lambda<Func<T, bool>>(
            notExpression,
            expression.Parameters.Single());
    }
}

And, Or, Not methods in the base class

public abstract class Specification<T>
{
    public static readonly Specification<T> All = new IdentitySpecification<T>();

    public bool IsSatisfiedBy(T entity)
    {
        Func<T, bool> predicate = ToExpression().Compile();
        return predicate(entity);
    }

    public abstract Expression<Func<T, bool>> ToExpression();

    public Specification<T> And(Specification<T> specification)
    {
        // Optimisation : And avec All (identité) = l'autre specification
        if (this == All)
            return specification;
        if (specification == All)
            return this;

        return new AndSpecification<T>(this, specification);
    }

    public Specification<T> Or(Specification<T> specification)
    {
        // Optimisation : Or avec All = toujours vrai (All)
        if (this == All || specification == All)
            return All;

        return new OrSpecification<T>(this, specification);
    }

    public Specification<T> Not()
    {
        return new NotSpecification<T>(this);
    }
}

Use in the ViewModel for dynamic queries

private void Search()
{
    // Démarrer avec l'identité (tout vrai)
    Specification<Movie> spec = Specification<Movie>.All;

    // Ajouter des conditions dynamiquement via And()
    if (ForKidsOnly)
    {
        spec = spec.And(new MovieForKidsSpecification());
    }
    if (OnCD)
    {
        spec = spec.And(new AvailableOnCDSpecification());
    }

    Movies = _repository.GetList(spec, MinimumRating);
    Notify(nameof(Movies));
}

The IdentitySpecification (Specification<T>.All) is an implementation of the Null Object Pattern: it acts as if no specification was applied (returns true for any object). It allows you to build dynamic queries without having to check if the specification is null.


4.4 Summary of the combination of Specifications

Specification combining is a very powerful mechanism for creating sophisticated queries from simple building blocks.

  • 3 types of combination: And, Or, Not
  • Implemented via manipulation of C# expression trees (Expression.AndAlso, Expression.OrElse, Expression.Not)
  • Combination classes are internal sealed — inaccessible directly to client code, which passes through base class methods
  • IdentitySpecification<T> implements Null Object Pattern for dynamic queries

4.5 When not to use the Specification Pattern

Be careful not to treat this pattern like a hammer seeking a nail. There are cases where it is better to do without it.

Case 1: Only one use case out of three

If you only need database lookup but not in-memory validation, there is no duplication of knowledge — the logic is only used in one place. Put the filtering logic directly in the repository.

Likewise, if you only need in-memory validation but no database search, code directly in your ifs.

// ✅ Acceptable si usage unique — pas de duplication
public IReadOnlyList<Movie> GetKidsMovies()
{
    return session.Query<Movie>()
        .Where(x => x.MpaaRating <= MpaaRating.PG)
        .ToList();
}

Case 2: Simple application with little expected growth

In small applications, the maintenance cost is already low. The benefit of introducing the Specification Pattern may not justify the initial investment.

When to use the pattern

Use the Specification Pattern if your code has the need for at least 2 of the 3 use cases, and if your project is intended to become or remain complex.


4.6 Combine Specifications and Ordinary Filtration

Not all filter options need to be Specifications. If an attribute is used only for base searching (without the need for in-memory validation), it is perfectly acceptable to implement this filtering directly in the repository.

Example: filtering by minimum score

The minimum score is a pure search criterion — it is not needed to validate purchases. We can therefore manage it directly in the repository:

public IReadOnlyList<MovieDto> GetList(
    Specification<Movie> specification,
    double minimumRating,      // Filtration ordinaire — pas une Specification
    int page = 0,
    int pageSize = 20)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        return session.Query<Movie>()
            .Where(specification.ToExpression())   // Specification
            .Where(x => x.Rating >= minimumRating) // Filtration ordinaire
            .Skip(page * pageSize)                 // Pagination
            .Take(pageSize)
            .Fetch(x => x.Director)
            .ToList()
            .Select(x => new MovieDto { ... })
            .ToList();
    }
}

Pagination

Paging is another example of “ordinary filtering” that fits naturally with the Specifications:

// Utilisation dans le ViewModel
private void Search()
{
    Specification<Movie> spec = Specification<Movie>.All;

    if (ForKidsOnly)
        spec = spec.And(new MovieForKidsSpecification());

    if (OnCD)
        spec = spec.And(new AvailableOnCDSpecification());

    // MinimumRating passé séparément, page et pageSize pour la pagination
    Movies = _repository.GetList(spec, MinimumRating, page: 0, pageSize: 20);
    Notify(nameof(Movies));
}

4.7 Working with multiple classes

Adding a Director entity

Specifications can be used with properties of associated classes (navigation properties). Example: every film now has a director.

// Entité Director
public class Director : Entity
{
    public virtual string Name { get; }
}

// Mapping FluentNHibernate
public class DirectorMap : ClassMap<Director>
{
    public DirectorMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);
    }
}
// Movie avec navigation property Director
public class Movie : Entity
{
    public virtual string Name { get; }
    public virtual DateTime ReleaseDate { get; }
    public virtual MpaaRating MpaaRating { get; }
    public virtual string Genre { get; }
    public virtual double Rating { get; }
    public virtual Director Director { get; }   // ← relation

    protected Movie() { }
}

// Mapping Movie avec référence Director
public class MovieMap : ClassMap<Movie>
{
    public MovieMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Map(x => x.ReleaseDate);
        Map(x => x.MpaaRating).CustomType<int>();
        Map(x => x.Genre);
        Map(x => x.Rating);
        References(x => x.Director);  // ← clé étrangère
    }
}

SQL script with Director table

CREATE TABLE [dbo].[Director](
    [DirectorID] [int] IDENTITY(1,1) NOT NULL,
    [Name]       [nvarchar](50) NOT NULL,
    CONSTRAINT [PK_Director] PRIMARY KEY CLUSTERED ([DirectorID] ASC)
)

CREATE TABLE [dbo].[Movie](
    [MovieID]     [bigint] IDENTITY(1,1) NOT NULL,
    [Name]        [nvarchar](50) NOT NULL,
    [ReleaseDate] [datetime] NOT NULL,
    [MpaaRating]  [int] NOT NULL,
    [Genre]       [varchar](50) NOT NULL,
    [Rating]      [float] NOT NULL,
    [DirectorID]  [int] NOT NULL,   -- ← clé étrangère
    CONSTRAINT [PK_Movie] PRIMARY KEY CLUSTERED ([MovieID] ASC)
)

ALTER TABLE [dbo].[Movie]
    ADD CONSTRAINT [FK_Movie_Director]
    FOREIGN KEY([DirectorID]) REFERENCES [dbo].[Director] ([DirectorID])

-- Réalisateurs
INSERT [dbo].[Director] VALUES (1, N'Marc Webb')
INSERT [dbo].[Director] VALUES (2, N'Bill Condon')
INSERT [dbo].[Director] VALUES (3, N'Chris Renaud')
INSERT [dbo].[Director] VALUES (4, N'Jon Favreau')
INSERT [dbo].[Director] VALUES (5, N'M. Night Shyamalan')
INSERT [dbo].[Director] VALUES (6, N'Alex Kurtzman')

Specification using an associated class

// Specification sur une propriété de navigation
public sealed class MovieDirectedBySpecification : Specification<Movie>
{
    private readonly string _director;

    public MovieDirectedBySpecification(string director)
    {
        _director = director;
    }

    public override Expression<Func<Movie, bool>> ToExpression()
    {
        return movie => movie.Director.Name == _director;
    }
}

Data Transfer Object (DTO)

Rather than directly exposing the Movie entity to the UI layer, we use a MovieDto which flattens the data:

public class MovieDto
{
    public long Id { get; set; }
    public string Name { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string MpaaRating { get; set; }
    public string Genre { get; set; }
    public double Rating { get; set; }
    public string Director { get; set; }  // ← string au lieu de Director entity
}

Important point: Eager Loading

When using specifications on associated classes, you must ensure that these associated objects are loaded in advance (eager loading). By default, ORMs use lazy loading, which can cause exceptions if the associated object is not available.

// .Fetch(x => x.Director) force l'eager loading de Director
return session.Query<Movie>()
    .Where(specification.ToExpression())
    .Fetch(x => x.Director)   // ← eager loading
    .ToList()
    .Select(x => new MovieDto { ... })
    .ToList();

4.8 Creating new objects — 3rd use case

The third use case of the Specification Pattern is the creation of new objects. Martin Fowler and Eric Evans describe it this way:

“A way of describing what an object could do, without explaining the details of how the object does it, but in such a way that a candidate could be constructed to satisfy the requirement.”

Concrete example

A film company identifies a gap in the children’s film market and wants to create a new film for that audience. By composing different specifications, she can ask the software to construct an example of what this movie might look like — to get initial ideas.

How it works

This use case is a subset of in-memory validation:

  • In normal validation: the object is provided by the customer (it already exists)
  • In creation: we start from the set of all possible objects as a starting point, and we validate each of them separately
Ensemble de candidats possibles
        ↓
Validation par les Specifications
        ↓
Premier candidat satisfaisant la Specification

The main constraint: brute force

To satisfy the requirements of a specification when creating objects, you must:

  1. Generate an initial candidate set
  2. Validate each via IsSatisfiedBy()
  3. Return the first (or all) that meet the conditions

The challenge is not the validation itself, but creating the initial set of candidates to validate.

// Conceptuellement
IEnumerable<Movie> GenerateCandidates()
{
    // Générer toutes les combinaisons possibles d'attributs
    foreach (var rating in Enum.GetValues<MpaaRating>())
    {
        foreach (var genre in allGenres)
        {
            yield return new Movie(/* ... paramètres ... */);
        }
    }
}

// Trouver le premier qui satisfait la specification
var spec = new MovieForKidsSpecification()
    .And(new AvailableOnCDSpecification());

Movie example = GenerateCandidates()
    .First(movie => spec.IsSatisfiedBy(movie));

4.9 Summary of Module 4

This module covered the complete implementation of the correctly encapsulated Specification Pattern:

Key principles

  1. Strongly Typed Specifications actually contain domain knowledge — they do not accept it from the outside.
  2. Do not use ISpecification interfaces — they generally do not provide added value (YAGNI).
  3. Make the Specifications as specific as possible — do not introduce unnecessary variety.
  4. Make Specifications Immutable — instantiate a new specification rather than modifying the existing one.
  5. Combine Specifications via And, Or, Not to build sophisticated queries.
  6. The IdentitySpecification (Specification<T>.All) implements the Null Object Pattern for dynamic queries.
  7. Do not use the pattern if your use case concerns only one of the three scenarios (no duplication = no need).
  8. Combine Specifications and ordinary filtration — not all filter conditions need to be Specifications.
  9. Eager loading for associated classes to avoid lazy loading exceptions.

5. Resources and Conclusion

Course Conclusion

This course covered the entire lifecycle of the Specification Pattern in C#:

  1. Problem: duplication of domain knowledge in research and validation scenarios
  2. Naive approaches: raw C# expressions and Generic Specifications — and why they are insufficient
  3. Correct solution: Strongly Typed Specifications with combination via And/Or/Not
  4. Advanced use cases: working with aggregates, mixed filtration, object creation

Resources mentioned

  • Source code on GitHub (by Vladimir Khorikov)
  • Original article by Eric Evans and Martin Fowler on the Specification Pattern
  • DRY principle (Don’t Repeat Yourself)
  • YAGNI (You Aren’t Gonna Need It)
  • Liskov Substitution Principle (LSP)
  • Null Object Pattern (used for IdentitySpecification)

6. Full source code reference

Project architecture

SpecPattern.sln
└── src/
    ├── Logic/                        # Couche métier
    │   ├── Movies/
    │   │   ├── Movie.cs              # Entité principale
    │   │   ├── MovieMap.cs           # Mapping FluentNHibernate
    │   │   ├── MovieDto.cs           # Data Transfer Object
    │   │   ├── MovieRepository.cs    # Repository
    │   │   ├── Specification.cs      # Classes de Specification
    │   │   ├── Director.cs           # Entité Director + mapping
    │   │   └── GenericSpecification.cs  # Anti-pattern (pour référence)
    │   └── Utils/
    │       ├── Entity.cs             # Classe de base pour les entités
    │       ├── SessionFactory.cs     # Configuration NHibernate
    │       └── Initer.cs
    ├── UI/                           # Couche présentation WPF
    │   ├── Movies/
    │   │   └── MovieListViewModel.cs # ViewModel principal
    │   └── Common/
    │       ├── ViewModel.cs
    │       ├── Command.cs
    │       └── ...
    ├── Database.sql                  # Script SQL sans Director
    └── DatabaseWithDirector.sql      # Script SQL avec Director

Full code: Specification.cs (final version)

using System;
using System.Linq;
using System.Linq.Expressions;

namespace Logic.Movies
{
    // Null Object Pattern — retourne toujours true
    internal sealed class IdentitySpecification<T> : Specification<T>
    {
        public override Expression<Func<T, bool>> ToExpression()
        {
            return x => true;
        }
    }

    // Classe de base abstraite
    public abstract class Specification<T>
    {
        public static readonly Specification<T> All = new IdentitySpecification<T>();

        public bool IsSatisfiedBy(T entity)
        {
            Func<T, bool> predicate = ToExpression().Compile();
            return predicate(entity);
        }

        public abstract Expression<Func<T, bool>> ToExpression();

        public Specification<T> And(Specification<T> specification)
        {
            if (this == All)
                return specification;
            if (specification == All)
                return this;

            return new AndSpecification<T>(this, specification);
        }

        public Specification<T> Or(Specification<T> specification)
        {
            if (this == All || specification == All)
                return All;

            return new OrSpecification<T>(this, specification);
        }

        public Specification<T> Not()
        {
            return new NotSpecification<T>(this);
        }
    }

    // Combinaison AND
    internal sealed class AndSpecification<T> : Specification<T>
    {
        private readonly Specification<T> _left;
        private readonly Specification<T> _right;

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

        public override Expression<Func<T, bool>> ToExpression()
        {
            Expression<Func<T, bool>> leftExpression  = _left.ToExpression();
            Expression<Func<T, bool>> rightExpression = _right.ToExpression();

            BinaryExpression andExpression = Expression.AndAlso(
                leftExpression.Body,
                rightExpression.Body);

            return Expression.Lambda<Func<T, bool>>(
                andExpression,
                leftExpression.Parameters.Single());
        }
    }

    // Combinaison OR
    internal sealed class OrSpecification<T> : Specification<T>
    {
        private readonly Specification<T> _left;
        private readonly Specification<T> _right;

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

        public override Expression<Func<T, bool>> ToExpression()
        {
            Expression<Func<T, bool>> leftExpression  = _left.ToExpression();
            Expression<Func<T, bool>> rightExpression = _right.ToExpression();

            BinaryExpression orExpression = Expression.OrElse(
                leftExpression.Body,
                rightExpression.Body);

            return Expression.Lambda<Func<T, bool>>(
                orExpression,
                leftExpression.Parameters.Single());
        }
    }

    // Combinaison NOT
    internal sealed class NotSpecification<T> : Specification<T>
    {
        private readonly Specification<T> _specification;

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

        public override Expression<Func<T, bool>> ToExpression()
        {
            Expression<Func<T, bool>> expression = _specification.ToExpression();
            UnaryExpression notExpression = Expression.Not(expression.Body);

            return Expression.Lambda<Func<T, bool>>(
                notExpression,
                expression.Parameters.Single());
        }
    }

    // Specification métier : Films pour enfants
    public sealed class MovieForKidsSpecification : Specification<Movie>
    {
        public override Expression<Func<Movie, bool>> ToExpression()
        {
            return movie => movie.MpaaRating <= MpaaRating.PG;
        }
    }

    // Specification métier : Films disponibles en CD
    public sealed class AvailableOnCDSpecification : Specification<Movie>
    {
        private const int MonthsBeforeDVDIsOut = 6;

        public override Expression<Func<Movie, bool>> ToExpression()
        {
            return movie => movie.ReleaseDate <= DateTime.Now.AddMonths(-MonthsBeforeDVDIsOut);
        }
    }

    // Specification métier : Films d'un réalisateur spécifique
    public sealed class MovieDirectedBySpecification : Specification<Movie>
    {
        private readonly string _director;

        public MovieDirectedBySpecification(string director)
        {
            _director = director;
        }

        public override Expression<Func<Movie, bool>> ToExpression()
        {
            return movie => movie.Director.Name == _director;
        }
    }
}

Full code: MovieListViewModel.cs (final version)

using System.Collections.Generic;
using System.Windows;
using CSharpFunctionalExtensions;
using Logic.Movies;
using UI.Common;

namespace UI.Movies
{
    public class MovieListViewModel : ViewModel
    {
        private readonly MovieRepository _repository;

        public Command SearchCommand { get; }
        public Command<long> BuyAdultTicketCommand { get; }
        public Command<long> BuyChildTicketCommand { get; }
        public Command<long> BuyCDCommand { get; }
        public IReadOnlyList<MovieDto> Movies { get; private set; }

        public bool ForKidsOnly { get; set; }
        public double MinimumRating { get; set; }
        public bool OnCD { get; set; }

        public MovieListViewModel()
        {
            _repository = new MovieRepository();

            SearchCommand         = new Command(Search);
            BuyAdultTicketCommand = new Command<long>(BuyAdultTicket);
            BuyChildTicketCommand = new Command<long>(BuyChildTicket);
            BuyCDCommand          = new Command<long>(BuyCD);
        }

        private void BuyCD(long movieId)
        {
            Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
            if (movieOrNothing.HasNoValue)
                return;

            Movie movie = movieOrNothing.Value;
            var spec = new AvailableOnCDSpecification();
            if (!spec.IsSatisfiedBy(movie))
            {
                MessageBox.Show("The movie doesn't have a CD version", "Error",
                    MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            MessageBox.Show("You've bought a ticket", "Success",
                MessageBoxButton.OK, MessageBoxImage.Information);
        }

        private void BuyChildTicket(long movieId)
        {
            Maybe<Movie> movieOrNothing = _repository.GetOne(movieId);
            if (movieOrNothing.HasNoValue)
                return;

            Movie movie = movieOrNothing.Value;
            var spec = new MovieForKidsSpecification();
            if (!spec.IsSatisfiedBy(movie))
            {
                MessageBox.Show("The movie is not suitable for children", "Error",
                    MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            MessageBox.Show("You've bought a ticket", "Success",
                MessageBoxButton.OK, MessageBoxImage.Information);
        }

        private void BuyAdultTicket(long movieId)
        {
            MessageBox.Show("You've bought a ticket", "Success",
                MessageBoxButton.OK, MessageBoxImage.Information);
        }

        private void Search()
        {
            // Construction dynamique de la Specification
            Specification<Movie> spec = Specification<Movie>.All;

            if (ForKidsOnly)
                spec = spec.And(new MovieForKidsSpecification());

            if (OnCD)
                spec = spec.And(new AvailableOnCDSpecification());

            Movies = _repository.GetList(spec, MinimumRating);
            Notify(nameof(Movies));
        }
    }
}

Full code: Entity.cs — Base class

using System;
using NHibernate.Proxy;

namespace Logic.Utils
{
    public abstract class Entity
    {
        public virtual long Id { get; protected set; }

        public override bool Equals(object obj)
        {
            var other = obj as Entity;

            if (ReferenceEquals(other, null))
                return false;
            if (ReferenceEquals(this, other))
                return true;
            if (GetRealType() != other.GetRealType())
                return false;
            if (Id == 0 || other.Id == 0)
                return false;

            return Id == other.Id;
        }

        public static bool operator ==(Entity a, Entity b)
        {
            if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
                return true;
            if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
                return false;

            return a.Equals(b);
        }

        public static bool operator !=(Entity a, Entity b)
        {
            return !(a == b);
        }

        public override int GetHashCode()
        {
            return (GetRealType().ToString() + Id).GetHashCode();
        }

        private Type GetRealType()
        {
            return NHibernateProxyHelper.GetClassWithoutInitializingProxy(this);
        }
    }
}

Comparison of approaches — Summary table

ApproachDuplication eliminatedEncapsulationEasy combinationRecommendation
Logic in repository + ViewModelAnti-pattern
Entity Methods⚠️Doesn’t work with ORM
Static C# expressionsInsufficient
Generic SpecificationAnti-pattern
IQueryable from RepositoryAnti-pattern (violates LSP)
Strongly Typed SpecificationRecommended


Search Terms

specification · pattern · c-sharp · testing · patterns · architecture · c# · .net · development · specifications · repository · viewmodel · class · expressions · base · case · combination · entity · sql · strongly · typed · added · anti-pattern · classes

Interested in this course?

Contact us to book it or get a custom training plan for your team.