Search code examples
c#entity-frameworksortingpaginationspecification-pattern

Specification pattern with entity framework and using orderby and skip/take


I have picked up a project that uses the specification pattern, a pattern I have not used before, and I had to go and research the pattern. I have noticed it doesn't have OrderBy and Skip/Take functionality, and I can't find anywhere that shows how to implement this with the pattern.

I am struggling to think of how best to add this to the specification pattern. But I have hit issues, like the specification deals with "Expression<Func<T, bool>>" whereas I don't think I can store this along with orderby's etc

Basically there is a class like this:

public class Specification<T> : ISpecification<T>
{
    public Expression<Func<T, bool>> Predicate { get; protected set; }

    public Specification(Expression<Func<T, bool>> predicate)
    {
        Predicate = predicate;
    }

    public Specification<T> And(Specification<T> specification)
    {
        return new Specification<T>(this.Predicate.And(specification.Predicate));
    }

    public Specification<T> And(Expression<Func<T, bool>> predicate)
    {
        return new Specification<T>(this.Predicate.And(predicate));
    }

    public Specification<T> Or(Specification<T> specification)
    {
        return new Specification<T>(this.Predicate.Or(specification.Predicate));
    }

    public Specification<T> Or(Expression<Func<T, bool>> predicate)
    {
        return new Specification<T>(this.Predicate.Or(predicate));
    }

    public T SatisfyingItemFrom(IQueryable<T> query)
    {
        return query.Where(Predicate).SingleOrDefault();
    }

    public IQueryable<T> SatisfyingItemsFrom(IQueryable<T> query)
    {
        return query.Where(Predicate);
    }
}

This allows to create a specification, passing in a where clause. It also allows chaining of rules with the "And", "Or". For example:

var spec = new Specification<Wave>(w => w.Id == "1").And(w => w.WaveStartSentOn > DateTime.Now);

How can I add a method for "OrderBy" and "Take"?

As this is existing code, I can't do any changes that would affect existing code, and it would be quite a job to refactor it. So any solution would need to play nicely with what is there.


Solution

  • How about

    public class Specification<T> : ISpecification<T>
    {
        public Expression<Func<T, bool>> Predicate { get; protected set; }
        public Func<IQueryable<T>, IOrderedQueryable<T>> Sort {get; protected set; }
        public Func<IQueryable<T>, IQueryable<T>> PostProcess {get; protected set;
    
        public Specification<T> OrderBy<TProperty>(Expression<Func<T, TProperty>> property)
        {
            var newSpecification = new Specification<T>(Predicate) { PostProcess = PostProcess } ;
            if(Sort != null) {
                 newSpecification.Sort = items => Sort(items).ThenBy(property);
            } else {
                 newSpecification.Sort = items => items.OrderBy(property);
            }
            return newSpecification;
        }
    
        public Specification<T> Take(int amount)
        {
            var newSpecification = new Specification<T>(Predicate) { Sort = Sort } ;
            if(PostProcess!= null) {
                 newSpecification.PostProcess= items => PostProcess(items).Take(amount);
            } else {
                 newSpecification.PostProcess= items => items.Take(amount);
            }
            return newSpecification;
        }
    
        public Specification<T> Skip(int amount)
        {
            var newSpecification = new Specification<T>(Predicate) { Sort = Sort } ;
            if(PostProcess!= null) {
                 newSpecification.PostProcess= items => PostProcess(items).Skip(amount);
            } else {
                 newSpecification.PostProcess= items => items.Skip(amount);
            }
            return newSpecification;
        }
    }
    

    TODO:

    • similar construction for OrderByDescending
    • Update your other methods so the "Sort" value and "PostProcess" value is not lost when you call "And", for example

    then your Satisfying methods become:

    private IQueryable<T> Prepare(IQueryable<T> query) 
    {
        var filtered = query.Where(Predicate);
        var sorted = Sort(filtered);
        var postProcessed = PostProcess(sorted);
        return postProcessed;
    }
    
    public T SatisfyingItemFrom(IQueryable<T> query)
    {
        return Prepare(query).SingleOrDefault();
    }
    
    public IQueryable<T> SatisfyingItemsFrom(IQueryable<T> query)
    {
        return Prepare(query);
    }
    

    TODO: check if Sort & PostProcess are not null in the "Prepare" method

    Usage:

    var spec = new Specification<Wave>(w => w.Id == "1")
                  .And(w => w.WaveStartSentOn > DateTime.Now)
                  .OrderBy(w => w.WaveStartSentOn)
                  .Skip(20)
                  .Take(5);