Using Specification Pattern and Expressions in Entity Framework and Linq to Entities

Viewed 2139

I have a number of complex queries in a repository accesing a DB via Entity Framework / Linq-to-Entities. These queries, generally, are built up out of a number of non-trivial sub-queries. Generally speaking, the sub-queries are used across different repository methods and also in other domain logic. It makes sense for them to be external to, but accessible to, the repository layer.

As such I want to use the specification pattern to encapsulate some of these sub-queries.

I am using a base class for my specification classes:

public abstract class Specification<T> : ISpecification<T> where T : class
{
    public abstract Expression<Func<T, bool>> ToExpression();

    public virtual bool IsSatisfiedBy(T candidate)
    {        
        var predicate = ToExpression().Compile();
        return predicate(candidate);
    }

    public Specification<T> And(Specification<T> specification)
    {
        return new AndSpecification<T>(this, specification);
    }

    public Specification<T> Or(Specification<T> specification)
    {
        return new OrSpecification<T>(this, specification);
    }
}

A sample specification might look something like this:

 public class IsAssignmentSetForStudentSpecification : Specification<Assignment>
{
    private readonly Student _student;

    public IsAssignmentSetForStudentSpecification(Student student)
    {
        _student = student;
    }

    public override Expression<Func<Assignment, bool>> ToExpression()
    {
        return x => !x.Exclusions.Contains(_student) &&
                    (
                        _student.Classes.Select(c => c.Subject).Intersect(x.Subjects).Any() ||
                        x.TutorGroups.Contains(_student.TutorGroup) ||
                        x.Houses.Contains(_student.House) ||
                        x.YearGroups.Contains(_student.YearGroup) ||
                        x.Students.Contains(_student)
                    );
    }
}

As you can see I would prefer not to be writing such code in each repository query.

As repository query method (using various specifications) might look like this:

public ICollection<Assignment> GetAssignmentsDueInForStudent(Student student, DateRange dateRange)
{
    var isAssignmentAssignedToStudent = new IsAssignmentSetForStudentSpecification(student);
    var isAssignmentDueInDateRange = new IsAssignmentDueInDateRangeSpecification(dateRange);
    var hasStudentCompletedAssignment = new HasStudentCompletedAssignmentSpecification(student);

    return (from a in Set
                    .Where(x => isAssignmentAssignedToStudent
                        .And(isAssignmentDueInDateRange).IsSatisfiedBy(x))
                    .Where(x => !hasStudentCompletedAssignment.IsSatisfiedBy(x))
                select a)
            .ToList(queryOptions);
}

In the above method, Set is an IDbSet<>

Unfortunately, when I run the query, I get the following error:

LINQ to Entities does not recognize the method 'Boolean IsSatisfiedBy(Beehive.Domain.Planner.Assignments.Assignment)' method, and this method cannot be translated into a store expression.

How can I get around this?

1 Answers

With the specification pattern, you can move domain logic out of your repositories. You could have a very thin repository that looks something like:

public ICollection<Assignment> List(ISpecification<Assignment> specification)
{    
    return (from a in Set
                    .Where(specification.ToExpression())
                select a)
            .ToList(queryOptions);
}

You could then use the And Or and Not operations in your specification classes to combine specificaitons before passing them in.

var isAssignmentDueForStudent = new isAssignmentSetForStudentSpecification(student)
    .And(new IsAssignmentDueInDateRangeSpecification(dateRange))
    .And(new HasStudentCompletedAssignmentSpecification(student).Not());

return assignmentRepository.List(isAssignmentDueForStudent);

That keeps the logic out of your repository and the data layer doesn't have a dependency on your specification implementations. Also when you implement the AndSpecifications etc, you are overriding ToExpression to combine expressions, not IsSatisfiedBy which probably doesn't need to be virtual.

Related