4

I have a DbContext with a lot of DbSets. Every DbSet should have a function to get a page of items from the set, with a given pageSize and ordered by a specific sortOrder. Something like:

var pageItems = dbContext.Posts
    .Where(post => post.BlogId == blogId)
    .OrderBy(some sortorder)
    .Skip(pageNr * pageSize)
    .Take(pageSize);

I want to be able to do this with all my DbSets, so I have created an extension method where one of the parameters specifies the foreign key to compare and another the value this foreign key should have.

public static IQueryable<TSource> GetPage<TSource>(this IQueryable<TSource> source,
    int pageNr, int pageSize,
    Expression<Func<TSource, Tproperty>> keySelector, Tproperty comparisonValue)
{
    return  source
    .Where( ??? )
    .OrderBy(some sortorder)
    .Skip(pageNr * pageSize)
    .Take(pageSize);
}

How to convert the keySelector in a predicate suitable for Where?

Martijn
  • 11,964
  • 12
  • 50
  • 96
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Jon Skeet arriving – Sarvesh Mishra Jul 14 '16 at 10:34
  • You can try to use `Expression.LessThan` and take pass it body of your `keySelector` and `comprasionValue` converted to `Expression.Constant` – Guru Stron Jul 14 '16 at 10:36
  • Let say the `Where` is resolved. How you will resolve the `OrderBy`? – Ivan Stoev Jul 14 '16 at 10:49
  • 1
    The *easier* way to do this is just to pass the expression for the predicate rather than the expression for the keySelector and the comparisonValue separately. – Martijn Jul 14 '16 at 11:06
  • 1
    In my opinion it is an unnecessary layer of abstraction. It not only does not save you much code since you still need to write the selections and orders etc, but also it will limit the flexibility and readability. – Stephen Zeng Jul 14 '16 at 13:11
  • @IvanStoev: the OrderBy is in a separate parameter. I left it out in my question, because that is already solved. The easiest method would be to pass the expression that is used to OrderBy, however that would mean that users would have to know the structure of the data. Another method could be to use some enum to indicate how to sort: OrderByName, OrderByBirthdate, etc. – Harald Coppoolse Jan 13 '20 at 15:19

5 Answers5

3

How to convert the keySelector in a predicate suitable for Where?

This is quite easy, but I have no idea how are you going to handle ordering. Anyway, here is how you can do what are you asking for:

public static IQueryable<TSource> GetPage<TSource, TKey>(this IQueryable<TSource> source,
    int pageNr, int pageSize,
    Expression<Func<TSource, TKey>> keySelector, TKey comparisonValue)
{
    var predicate = Expression.Lambda<Func<TSource, bool>>(
        Expression.Equal(keySelector.Body, Expression.Constant(comparisonValue)),
        keySelector.Parameters);

    return source
        .Where(predicate)
        //.OrderBy(some sortorder) ??
        .Skip(pageNr * pageSize)
        .Take(pageSize);
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
2

You're looking for a way to get an Expression<Func<TSource, boolean>> from an Expression<Func<TSource, Tproperty>> keySelector and a Tproperty comparisonValue in such a way that it can be translated in to a store expression by the Entity Framework.

That means that the trivial

public static Expression<Func<TSource, bool>> KeyPredicateNaive<TSource, Tproperty>(Expression<Func<TSource, Tproperty>> keySelector, Tproperty comparisonValue)
{
  return (TSource source) =>EqualityComparer<Tproperty>.Default.Equals(keySelector.Compile()(source), comparisonValue);
}

won't work. This can't be translated to a store expression.

We need to construct the expression manually. What we need is an equality expression with the key selector as its left value, and a constant expression with the comparison value as value as its right value. We can construct that as follows:

public static Expression<Func<TSource, bool>> KeyPredicate<TSource, Tproperty>(Expression<Func<TSource, Tproperty>> keySelector, Tproperty comparisonValue)
{
  var bd = Expression.Equal(keySelector.Body, Expression.Constant(comparisonValue));
  return Expression.Lambda<Func<TSource, bool>>(bd, keySelector.Parameters);
}

The result of that can be passed to your where class. Slimmed down (so that it'll compile and run), your method will look like

public static IQueryable<TSource> GetPage<TSource>(this IQueryable<TSource> source,
    int pageSize,
    Expression<Func<TSource, Tproperty>> keySelector, Tproperty comparisonValue)
{
    return source
    .Where(KeyPredicate(keySelector, comparisonValue)
    .Take(pageSize);
}

Would I use this? Probably not. It's easier all round to pass the predicate as a lambda to the function directly rather than constructing the expression yourself. But it's certainly a possibility.

Martijn
  • 11,964
  • 12
  • 50
  • 96
  • This method works. I already found some articles about creating expression like you did. However I couldn't find how to create the lambda expression out of the BinaryExpresson. You are right, it is easier to pass the Func instead of passing Func, and it gives the same level of type checking. – Harald Coppoolse Jul 14 '16 at 13:36
  • Still, it's good to have the technique in your toolbelt, so next time it'll be easier to determine whether it's a good idea or not if you're thinking about something similar. – Martijn Jul 14 '16 at 13:55
  • Same as this answer, given 2 hours earlier http://stackoverflow.com/a/38372514/360211 – weston Jul 14 '16 at 15:14
  • @weston that's pretty much a code-only answer, without explaining anything about it. – Martijn Jul 14 '16 at 15:46
0

You are writing extension to queryable source, yes? So just pass expressions and filter source:

public static IQueryable<TSource> GetPage<TSource, TKey>(this IQueryable<TSource> source,
    Expression<Func<TSource, bool>> predicate,
    Expression<Func<TSource, TKey>> keySelector,
    int pageNr, int pageSize
    )
{
    return  source
       .Where(predicate)
       .OrderBy(keySelector)
       .Skip(pageNr * pageSize)
       .Take(pageSize);
}

Usage:

db.Posts.GetPage(p => p.Author == "Bob", p => p.Date, 5, 10);

Note: in your approach you have problem with sorting (second expression) and all you get is passing two parameters p => p.Author, "Bob" instead of passing one ready-to use expression p => p.Author == "Bob".


But I would move predicate and keySelector out of GetPage method. Let this method focus on paging only (as method name states):

public static IQueryable<TSource> GetPage<TSource, TKey>(this IQueryable<TSource> source,
    int pageNr, int pageSize)
{
    return  source.Skip(pageNr * pageSize).Take(pageSize);
}

Usage:

db.Posts.Where(p => p.Author == "Bob").OrderBy(p => p.Date).GetPage(5, 10);

Or if you have repository

postsRepository.GetByAuthor("Bob").GetPage(5, 10);
Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
0

try this and see if it helps

Expression<Func<TSource, bool>> keySelector

or simply

Func<TSource, bool> keySelector
HGMamaci
  • 1,339
  • 12
  • 20
0

Given this code:

sealed class SwapVisitor : ExpressionVisitor
{
    private readonly Expression _from;
    private readonly Expression _to;

    public SwapVisitor(Expression from, Expression to)
    {
        _from = from;
        _to = to;
    }

    public override Expression Visit(Expression node)
    {
        return node == _from ? _to : base.Visit(node);
    }
}

static Expression<Func<TInput, bool>> Combine<TInput, TOutput>(
    Expression<Func<TInput, TOutput>> transform,
    Expression<Func<TOutput, bool>> predicate)
{
    var swap = new SwapVisitor(predicate.Parameters[0], transform.Body);
    return Expression.Lambda<Func<TInput, bool>>(
        swap.Visit(predicate.Body), transform.Parameters);
}

You can:

 .Where(Combine(keySelector, key => key == comparisonValue))

So that is creating a new Expression, with the body of the passed expression keySelector and the new expression for the comparison.

Thanks to Combine Lambda Expressions

Community
  • 1
  • 1
weston
  • 54,145
  • 21
  • 145
  • 203