Level: Intermediate
Table of Contents
- What is the Specification Pattern?
- Use case
- Presentation of the example project
- Added new search options
- Added new purchase options
- Code review and identified issues
- Module 2 Summary
- How LINQ works
- Use simple C# expressions
- Limits of simple C# expressions
- Generic Specifications
- Limits of Generic Specifications
- Return IQueryable from a Repository — Anti-pattern
- Module 3 Summary
- Strongly Typed Specifications
- General Guidelines
- Combination of Specifications
- Specifications combination summary
- When not to use the Specification Pattern
- Combiner Specifications and ordinary filtration
- Working with multiple classes
- Creating new objects — 3rd use case
- Module 4 Summary
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:
- Avoid duplication of domain knowledge in certain scenarios — in other words, follow the DRY (Don’t Repeat Yourself) principle.
- 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 cases | Description |
|---|---|---|
| 1 | Memory validation | Check if an available item meets certain criteria. Useful when validating incoming requests from users or third-party systems. |
| 2 | Retrieving data from database | Find records that match the specification. |
| 3 | Creation of new objects | Create 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 —
MovieListViewModelhandles 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:
- Kids’ Movies (
ForKidsOnly): only show movies with anMpaaRatingof G or PG - Minimum Rating (
MinimumRating): filter by rating - 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
MpaaRatingis an enum, we can use the comparison operator<=because its underlying database value is an integer. The ORM transforms thePGvalue 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:
- A child’s ticket — only for
GorPGfilms - 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
| Interface | Usage | Predicate type |
|---|---|---|
IEnumerable<T> | Manipulating objects in memory | Func<T, bool> (delegate) |
IQueryable<T> | Generation of SQL queries | Expression<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
| Delegate | Phrase | |
|---|---|---|
| Compiled in | Executable IL code | Syntax tree (AST) |
| Execution | By the processor | Parsed at runtime, translated into SQL |
| Conversion | Delegate → Expression: impossible | Expression → 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
Expressionrepresents a code tree (AST) which can be explored at runtime. This is what the ORM uses to generate the SQL. We can compile anExpressionintoFunc(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");
}
Building dynamic queries in Search()
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
IQueryablefrom public methods. Likewise, preferIReadOnlyList<T>toIEnumerable<T>for collections returned by repositories, becauseIEnumerablecan hide unwanted deferred execution.
3.7 Module 3 Summary
- LINQ relies on two interfaces:
IEnumerable(in memory) andIQueryable(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
IQueryablefrom 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
| Operation | Internal class | Description |
|---|---|---|
And | AndSpecification<T> | Both conditions must be true |
Gold | OrSpecification<T> | At least one condition must be true |
Not | NotSpecification<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 (returnstruefor 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:
- Generate an initial candidate set
- Validate each via
IsSatisfiedBy() - 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
- Strongly Typed Specifications actually contain domain knowledge — they do not accept it from the outside.
- Do not use
ISpecificationinterfaces — they generally do not provide added value (YAGNI). - Make the Specifications as specific as possible — do not introduce unnecessary variety.
- Make Specifications Immutable — instantiate a new specification rather than modifying the existing one.
- Combine Specifications via
And,Or,Notto build sophisticated queries. - The IdentitySpecification (
Specification<T>.All) implements the Null Object Pattern for dynamic queries. - Do not use the pattern if your use case concerns only one of the three scenarios (no duplication = no need).
- Combine Specifications and ordinary filtration — not all filter conditions need to be Specifications.
- 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#:
- Problem: duplication of domain knowledge in research and validation scenarios
- Naive approaches: raw C# expressions and Generic Specifications — and why they are insufficient
- Correct solution: Strongly Typed Specifications with combination via
And/Or/Not - 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
| Approach | Duplication eliminated | Encapsulation | Easy combination | Recommendation |
|---|---|---|---|---|
| Logic in repository + ViewModel | ❌ | ❌ | ❌ | Anti-pattern |
| Entity Methods | ✅ | ⚠️ | ❌ | Doesn’t work with ORM |
| Static C# expressions | ✅ | ❌ | ❌ | Insufficient |
| Generic Specification | ✅ | ❌ | ❌ | Anti-pattern |
| IQueryable from Repository | ✅ | ❌ | ✅ | Anti-pattern (violates LSP) |
| Strongly Typed Specification | ✅ | ✅ | ✅ | Recommended |
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