In this post I’m going to create a generic implementation of
the specification pattern.
The central idea of Specification is to separate the
statement of how to match a candidate, from the candidate object that it is
matched against. As well as its usefulness in selection, it is also valuable
forvalidation and for building to order. Every specification implementation
needs to specify a method that evaluates a condition mentioned in the interface
Ispecification as given below.
public interface ISpecification where
T : class
{
Expression<Funcbool>> SatisfiedBy();
}
The generic specification class implements the Ispecification
interface as
public sealed class Specification
: ISpecification where T : class
{
readonly Expression<Funcbool>>
_matchingCriteria;
public Specification(Expression<Funcbool>>
matchingCriteria)
{
if (matchingCriteria == null)
throw new ArgumentNullException("matchingCriteria");
_matchingCriteria = matchingCriteria;
}
public Expression<Funcbool>>
SatisfiedBy()
{
return _matchingCriteria;
}
}
Using this implementation we can create the AND
specification as
public sealed class AndSpecification
: ISpecification where T : class
{
private readonly ISpecification _rightSideSpecification;
private readonly ISpecification _leftSideSpecification;
public AndSpecification(ISpecification
leftSide, ISpecification rightSide)
{
if (leftSide == null)
throw new ArgumentNullException("leftSide");
if (rightSide == null)
throw new ArgumentNullException("rightSide");
_leftSideSpecification = leftSide;
_rightSideSpecification = rightSide;
}
public ISpecification
LeftSideSpecification
{
get { return
_leftSideSpecification; }
}
public ISpecification
RightSideSpecification
{
get { return
_rightSideSpecification; }
}
public Expression<Funcbool>>
SatisfiedBy()
{
var left = _leftSideSpecification.SatisfiedBy();
var right = _rightSideSpecification.SatisfiedBy();
return (left.And(right));
}
}
The AND method is an extension method on the ISpecification and
Expression<Funcbool>>
types
public static ISpecification AND(this ISpecification
leftSpecification, ISpecification
rightSpecification) where T : class
{
return new AndSpecification(leftSpecification,
rightSpecification);
}
public static Expression Compose(this Expression
first, Expression second,
Func<Expression,
Expression, Expression>
merge)
{
var map =
first.Parameters.Select((f, i) => new
{f, s = second.Parameters[i]}).ToDictionary(p => p.s, p => p.f);
var secondBody = ExpressionTreeParameterReplacer.ReplaceParameters(map,
second.Body);
return Expression.Lambda(merge(first.Body,
secondBody), first.Parameters);
}
public static Expression<Funcbool>> AND(this Expression<Funcbool>>
first,
Expression<Funcbool>> second)
{
return first.Compose(second, Expression.And);
}
The ExpressionTreeVisitor implementation to replace and join
the parameters for these extension methods
public sealed class ExpressionTreeParameterReplacer
: ExpressionVisitor
{
private readonly Dictionary<ParameterExpression,
ParameterExpression> _map;
public ExpressionTreeParameterReplacer(Dictionary<ParameterExpression,
ParameterExpression> map)
{
_map
= map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression,
ParameterExpression> map,
Expression exp)
{
return new ExpressionTreeParameterReplacer(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression
p)
{
ParameterExpression replacement;
if (_map.TryGetValue(p, out
replacement))
p
= replacement;
return base.VisitParameter(p);
}
}
Now let’s look at a sample using the specification pattern
implementation.
public class Customer
{
public string
FirstName { get; set;
}
public string
LastName { get; set;
}
public string
UserName { get; set;
}
public DateTime
DateOfJoining { get; set;
}
public Address
Address { get; set;
}
}
public class Address
{
public string Street
{ get; set; }
public string State {
get; set; }
public string City { get; set; }
public string Country
{ get; set; }
}
public class CountryNameSpecification : ISpecification<Customer>
{
private readonly string _countryName;
public CountryNameSpecification(string countryName)
{
_countryName = countryName;
}
public Expression<Func<Customer,
bool>> SatisfiedBy()
{
return new Specification<Customer>(c
=> c.Address.Country.Contains(_countryName)).SatisfiedBy();
}
}
public class CustomerFirstNameSpecification :ISpecification<Customer>
{
private readonly string _firstName;
public
CustomerFirstNameSpecification(string
firstName)
{
_firstName = firstName;
}
public Expression<Func<Customer,
bool>> SatisfiedBy()
{
return new Specification<Customer>(c
=> c.FirstName.Equals(_firstName)).SatisfiedBy();
}
}
[TestMethod]
public void
AndSpecificationShouldSatisfyIfBothExpressionsSatisfiesTheCondition()
{
var customers = GetAllCustomers();
var fooCountrySpecification = new CountryNameSpecification("Foo");
var customerFirtNameSpecification = new CustomerFirstNameSpecification("Foo4");
var andSpecification =
fooCountrySpecification.AND(customerFirtNameSpecification);
var results =
customers.Where(andSpecification.SatisfiedBy());
Assert.IsTrue(results.ToList().All(c =>
c.Address.Country.Contains("Foo")
&& c.FirstName.Equals("Foo4")));
}
[TestMethod]
public void
OrSpecificationShouldSatisfyIfOnOfTheConditionsSatisfiesTheSet()
{
var customers = GetAllCustomers();
var fooCountrySpecification = new CountryNameSpecification("Foo");
var abcCountrySpecification = new CountryNameSpecification("Abc");
var pqrCountrySpecification = new CountryNameSpecification("Pqr");
var orSpecification =
fooCountrySpecification.OR(abcCountrySpecification).OR(pqrCountrySpecification);
var results =
customers.Where(orSpecification.SatisfiedBy());
Assert.IsTrue(results.Count() ==
customers.Count());
}
Where GetAllCustomers return an Iqueryable resultset of
customers with addresses.