Search code examples
entity-frameworkdynamic-linq

Passing an aggregate select expression to Dynamic Linq's GroupBy


I have simplified the following example from my code and hoping there's no obvious compilation errors because of it. Lets say I have the following entities (not what i actually have, please assume I have no EF or schema issues, this is just for example):

public class Company
{
  public string GroupProperty {get;set;}
  public virtual ICollection<PricingForm> PricingForms {get;set;}
}
public class PricingForm
{
  public decimal Cost {get;set;}
}

And I want to query like so:

IQueryable DynamicGrouping<T>(IQueryable<T> query)
{
  Expression<Func<Company, decimal?>> exp = c => c.PricingForms.Sum(fr => fr.Cost);
  string selector = "new (it.Key as Key, @0(it) as Value)";
  IQueryable grouping = query.GroupBy("it.GroupProperty", "it").Select(selector, exp);
  return grouping;
}

I get the following error when calling the groupby/select line:

System.Linq.Dynamic.ParseException: 'Argument list incompatible with lambda expression'

What type is "it" when grouped? I have tried using other expressions that assume it is an IGrouping<string, Company>, or a IQueryable<Company>, same error. I've tried just selecting "Cost" and moving the Sum() aggregate into the selector string (i.e. Sum(@0(it)) as Value) and always seem to get the same error.

I eventually tried something along the lines of:

Expression<Func<IEnumerable<Company>, decimal?>> exp = l => l.SelectMany(c => c.PricingForms).Sum(fr => fr.Cost);

However this one, I get farther but when attempting to iterate through the results I got a different error.

The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.

So, with this dynamic grouping and injecting my own select expression, what should I assume the datatype of 'it' is? Will this even work?


Solution

  • The type of it is IGrouping<TKey, TElement>, where TKey is dynamic based on the keySelector result type, and TElement is the element type of the input IQueryable. Luckily IGrouping<TKey, TElement> inherits (is a) IEnumerable<TElement>, so as soon as you know the input element type, you can safely base selector on IEnumerable<TElement>.

    In other words, the last attempt based on Expression<Func<IEnumerable<Company>, decimal?>> is correct.

    The new error you are getting is because @0(it) generates Expression.Invoke call which is not supported by EF. The easiest way to fix that is to use LINQKit Expand method:

    Expression<Func<Company, decimal?>> exp = c => c.PricingForms.Sum(fr => fr.Cost);
    string selector = "new (it.Key as Key, @0(it) as Value)";
    IQueryable grouping = query.GroupBy("it.GroupProperty", "it").Select(selector, exp);
    
    // This would fix the EF invocation expression error
    grouping = grouping.Provider.CreateQuery(grouping.Expression.Expand());
    
    return grouping;