Milos Mijatovic
Milos Mijatovic

Reputation: 975

GroupBy query by Linq.Expressions and Lambdas

What I need is to represent this query via Linq.Expressions:

db.Documents.GroupBy(a => 1).Select(b => b.Sum(c => c.Amount) });

Here is what I have so far:

IQueryable<Document> data = db.Documents;

ParameterExpression pe = Expression.Parameter(typeof(Document), "doc");

Expression groupBy = Expression.Call(
    typeof(Queryable),
    "GroupBy",
    new Type[] { typeof(Document), typeof(int) },
    data.Expression,
    Expression.Lambda(Expression.Constant(1), pe));

ParameterExpression peg = Expression.Parameter(typeof(IGrouping<int, Document>), "group");

Expression select = Expression.Call(
    typeof(Queryable),
    "Select",
    new Type[] { typeof(IGrouping<int, Document>), typeof(int) },
    groupBy,
    Expression.Lambda(Expression.Property(peg, "Key"), peg));

foreach (var item in data.Provider.CreateQuery(select)) { ... }

This was implementation of:

db.Documents.GroupBy(a => 1).Select(b => b.Key });

And it works perfectly. Now, I want to aggregate a sum instead of accessing the key of group.

That is where it gets tricky for me. I was thinking something like this:

ParameterExpression pe1 = Expression.Parameter(typeof(Document), "other");

Expression sum = Expression.Call(
    typeof(Queryable),
    "Sum",
    new Type[] { typeof(Document) },
    peg,
    Expression.Lambda(Expression.Property(pe1, "Amount"), pe1));

Also, for Sum function in

...b.Sum(c => c.Amount)

Intellisense gives signature:

IEnumerable<Document>.Sum<Document>(Func<Document, decimal> selector)

While for:

db.Documents.Sum(a => a.Amount)

I get:

IQueryable<Document>.Sum<Document>(Expression<Func<Document, decimal>> selector)

Selector is Func in one version and Expression in other. I don't know how to handle Func in Linq Expressions. Maybe Intellisense is wrong?

Expression for source of aggregation is my biggest issue. By looking at:

...b.Sum(c => c.Amount)

i would presume that b should be IGrouping (ParameterExpression of 'select'), and that should be the source for Sum, but that won't compile. I don't know what else to try?

Here is how last select expression should look like:

Expression Select = Expression.Call(
    typeof(Queryable),
    "Select",
    new Type[] { typeof(IGrouping<int, Document>), typeof(decimal?) },
    GroupBy,
    Expression.Lambda(sum, peg));

But I can't even reach this point, because of the failed 'sum' expression.

Any pointers would be appreciated.

Regards,

Upvotes: 2

Views: 1360

Answers (1)

Ivan Stoev
Ivan Stoev

Reputation: 205539

The Intellisense is ok. Let see:

db.Documents.GroupBy(a => 1).Select(b => b.Sum(c => c.Amount) });

(1) db.Documents type is IQueryable<Document>
(2) a type is Document
(3) db.Documents.GroupBy(a => 1) type is IQueryable<IGrouping<int, Document>>
(4) b type is IGrouping<int, Document>, which in turn is IEnumerable<Document>
(5) c type is Document

which also means that GroupBy and Select methods are from Queryable while Sum is from Enumerable.

What about how to distinguish between Func<...> and Expression<Func<...>> inside the MethodCall expressions, the rule is simple. In both cases you use Expression.Lambda<Func<...>> to create Expression<Func<...>>, and then if the call requires Func<...> you pass it directly, and if the method expects Expression<Func<...>> then you wrap it with Expression.Quote.

With that being said, let build the sample query expression:

var query = db.Documents.AsQueryable();
// query.GroupBy(a => 1)
var a = Expression.Parameter(typeof(Document), "a");
var groupKeySelector = Expression.Lambda(Expression.Constant(1), a);
var groupByCall = Expression.Call(typeof(Queryable), "GroupBy",
    new Type[] { a.Type, groupKeySelector.Body.Type },
    query.Expression, Expression.Quote(groupKeySelector));
// c => c.Amount
var c = Expression.Parameter(typeof(Document), "c");
var sumSelector = Expression.Lambda(Expression.PropertyOrField(c, "Amount"), c);
// b => b.Sum(c => c.Amount)
var b = Expression.Parameter(groupByCall.Type.GetGenericArguments().Single(), "b");
var sumCall = Expression.Call(typeof(Enumerable), "Sum",
    new Type[] { c.Type },
    b, sumSelector);
// query.GroupBy(a => 1).Select(b => b.Sum(c => c.Amount))
var selector = Expression.Lambda(sumCall, b);
var selectCall = Expression.Call(typeof(Queryable), "Select",
    new Type[] { b.Type, selector.Body.Type },
    groupByCall, Expression.Quote(selector));
// selectCall is our expression, let test it
var result = query.Provider.CreateQuery(selectCall);

Upvotes: 5

Related Questions