- It allows to encapsulate some piece of domain knowledge into a single unit (specification) and reuse it in different parts of the code base.
- There are 3 main use cases for the Specification pattern:
- Looking up data in the database.
- That is finding records that match the specification we have in hand.
- Validating objects in the memory.
- In other words, checking that an object we retrieved or created fits the spec.
- Creating a new instance that matches the criteria.
- This is useful in scenarios where you don’t care about the actual content of the instances, but still, need it to have certain attributes.
- Generic specifications are a bad practice.
- If a specification allows you to indicate an arbitrary condition, it becomes just a container for the information which is passed to it by its client and doesn’t solve the underlying problem of domain knowledge encapsulation.
- Such specifications simply don’t contain any knowledge themselves.
- We can resolve this by using strongly-typed specifications that hard code the domain knowledge, with little or no possibility to alter it from the outside.
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
Func<T, bool> predicate = ToExpression().Compile();
return predicate(entity);
}
}
public class MovieRatingAtMostSpecification : Specification<Movie>
{
private readonly MovieRating _rating;
public MovieRatingAtMostSpecification(MovieRating rating) => _rating = rating;
public override Expression<Func<Movie, bool>> ToExpression() => (movie => movie.MovieRating <= _rating);
}
public void SomeMethod() // Controller
{
var gRating = new MovieRatingAtMostSpecification(MovieRating.G);
var isOk = gRating.IsSatisfiedBy(movie); // Exercising a single movie
IReadOnlyList<Movie> movies = repository.Find(gRating);
}
public IReadOnlyList<T> Find(Specification<T> specification) // Repository
{
using (ISession session = SessionFactory.OpenSession())
return session.Query<T>().Where(specification.ToExpression()).ToList();
}
- With this approach, we lift the domain knowledge to the class level making it much easier to reuse.
- No need to keep track of spec instances anymore: creating additional specification objects doesn’t lead to domain knowledge duplication, so we can do it freely.
- Also, it’s really easy to combine the specifications using And, Or, and Not methods.
public abstract class Specification<T>
{
public Specification<T> And(Specification<T> specification) => new AndSpecification<T>(this, specification);
public Specification<T> Or(Specification<T> specification) => new OrSpecification<T>(this, specification);
}
public 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());
}
}
public 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()
{
var leftExpression = _left.ToExpression();
var rightExpression = _right.ToExpression();
var paramExpr = Expression.Parameter(typeof(T));
var exprBody = Expression.OrElse(leftExpression.Body, rightExpression.Body);
exprBody = (BinaryExpression)new ParameterReplacer(paramExpr).Visit(exprBody);
var finalExpr = Expression.Lambda<Func<T, bool>>(exprBody, paramExpr);
return finalExpr;
}
}
- A question that is somewhat related to the specification pattern is: can repositories just return an
IQueryable<T>
?
- Wouldn’t it be easier to allow clients to query data from the backing store the way they want?
- This approach has essentially the same drawback as our initial specification pattern implementation.
- It encourages us to violate the DRY principle by duplicating the domain knowledge.
- This technique doesn’t offer us anything in terms of consolidating it in a single place.
- The second drawback here is that we are getting database notions leaking out of repositories.
- The implementation of
IQueryable<T>
highly depends on what LINQ provider is used behind the scene, so the client code should be aware that there potentially are queries which can’t be compiled into SQL.
- We are also getting a potential LSP violation.
IQueryables
are evaluated lazily, so we need to keep the underlying connection opened during the whole business transaction.
- Otherwise, the method will blow up with an exception.