Specification pattern with entity framework and using orderby and skip/take
Asked Answered
A

1

8

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.

Ashjian answered 11/8, 2014 at 13:6 Comment(0)
R
8

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);
Ricebird answered 11/8, 2014 at 13:17 Comment(7)
Thanks for the reply. Would this not have a problem of not keeping the order that things were chained in? In the sense that it is only storing ordering, it is not actually adding it to the query, for other actions to be called.Ashjian
I believe that the order will be respected. More specifically, this line should ensure that: if(Sort != null) { newSpecification.Sort = items => Sort(items).ThenBy(property); }Ricebird
Actually, I think the order of chaining won't matter, I am more thinking of scenarios you would encounter on the object side. Not in the DB side, which is what the specification is for! So I think what you propose would be fine.Ashjian
@Ricebird what does your interface look like?Cabot
Hah, good question, it would seem like the interface is quite redundant here. I don't have the original code anymore, but I would venture that it contained all the public methods, and that the Repository methods would accept an ISpecification<T> instead of a Specification.Ricebird
hi @Ricebird does this break the single responsibility class principle or does it matter? thanks one class is doing sorting, where, paging, etcPromotion
If you want to take this further, I've been using a customized version of EntitySorter and EntityFilter as described here: blogs.cuttingedge.it/steven/archive/66.htmlRicebird

© 2022 - 2024 — McMap. All rights reserved.