Monday, November 21, 2011

The Specification Pattern - A Generic implementation


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.

1 comment:

Anonymous said...

The example code does not compile.