向Dynamic Linq的GroupBy传递聚合选择表达式遇错求助
咱们一步步拆解你的问题,先搞清楚分组后it的类型,再分析错误根源,最后给出可行的实现方案。
分组后Select里的it到底是什么类型?
当你调用Dynamic LINQ的GroupBy("it.GroupProperty", "it")时,这个方法返回的是IQueryable<IGrouping<string, Company>>——这里的泛型参数是关键:
string是分组键的类型(因为Company.GroupProperty是string类型)Company是原查询的元素类型
而IGrouping<TKey, TSource>本身继承自IEnumerable<TSource>,所以在后续的Select动态语句中,it指代的就是单个IGrouping<string, Company>实例:它既包含分组的Key(也就是GroupProperty的值),也包含该分组下所有的Company对象集合。
你遇到的两个错误原因分析
第一个错误:Argument list incompatible with lambda expression
你最初定义的表达式exp是Expression<Func<Company, decimal?>>,它期望接收一个Company类型的参数;但在Select的动态字符串@0(it)里,it是IGrouping<string, Company>类型,和表达式的参数类型完全不匹配,自然会触发参数列表不兼容的错误。
第二个错误:The LINQ expression node type 'Invoke' is not supported in LINQ to Entities
后来你把表达式改成Expression<Func<IEnumerable<Company>, decimal?>>,参数类型虽然匹配了(因为IGrouping继承自IEnumerable<Company>),但Dynamic LINQ在处理@0(it)时,会生成一个Invoke表达式来调用你传入的外部表达式。而EF(假设你用的是Entity Framework)无法解析这种Invoke节点,所以抛出了不支持的错误。
可行的实现方案
针对你的场景,有两种靠谱的解决思路:
思路1:直接用Dynamic LINQ语法写聚合逻辑,避免外部表达式
既然Dynamic LINQ支持嵌套的LINQ方法调用,你可以把求和逻辑直接写到Select的动态字符串里,这样就不需要传递外部表达式,也不会触发Invoke的问题:
IQueryable DynamicGrouping<T>(IQueryable<T> query) { // 直接在动态字符串里编写嵌套的Sum逻辑 string selector = "new (it.Key as Key, it.Sum(c => c.PricingForms.Sum(fr => fr.Cost)) as Value)"; IQueryable grouping = query.GroupBy("it.GroupProperty", "it").Select(selector); return grouping; }
Dynamic LINQ会自动把这段字符串转换成EF能够解析的表达式树,完美避开类型不匹配和Invoke的问题。
思路2:手动构建表达式树,绕过Dynamic LINQ的参数替换限制
如果你必须保留外部表达式的灵活性,可以手动构建Select的表达式树,避免Dynamic LINQ生成Invoke节点。示例代码如下:
IQueryable DynamicGrouping<T>(IQueryable<T> query) { // 定义针对分组集合的聚合表达式 Expression<Func<IEnumerable<Company>, decimal?>> exp = l => l.SelectMany(c => c.PricingForms).Sum(fr => fr.Cost); // 获取分组后的元素类型:IGrouping<string, Company> var groupType = typeof(IGrouping<,>).MakeGenericType(typeof(string), typeof(Company)); // 定义参数:it(代表单个分组) var groupParam = Expression.Parameter(groupType, "it"); // 构建访问分组Key的表达式:it.Key var keyExpr = Expression.Property(groupParam, "Key"); // 构建聚合值的表达式:将exp应用到分组(因为IGrouping是IEnumerable<Company>) // 这里需要把exp的参数替换为groupParam,避免生成Invoke节点 var valueExpr = new ReplaceExpressionVisitor(exp.Parameters[0], groupParam).Visit(exp.Body); // 构建匿名类型的初始化表达式 var anonymousType = new { Key = default(string), Value = default(decimal?) }.GetType(); var memberBindings = new List<MemberBinding> { Expression.Bind(anonymousType.GetProperty(nameof(anonymousType.Key)), keyExpr), Expression.Bind(anonymousType.GetProperty(nameof(anonymousType.Value)), valueExpr) }; var initExpr = Expression.MemberInit(Expression.New(anonymousType), memberBindings); // 构建Select的Lambda表达式 var selectLambda = Expression.Lambda(initExpr, groupParam); // 执行分组和Select var grouping = query.GroupBy("it.GroupProperty", "it").Select(selectLambda); return grouping; } // 辅助类:用于替换表达式树中的参数 public class ReplaceExpressionVisitor : ExpressionVisitor { private readonly Expression _oldExpr; private readonly Expression _newExpr; public ReplaceExpressionVisitor(Expression oldExpr, Expression newExpr) { _oldExpr = oldExpr; _newExpr = newExpr; } public override Expression Visit(Expression node) { return node == _oldExpr ? _newExpr : base.Visit(node); } }
这个方案通过ReplaceExpressionVisitor把外部表达式的参数替换成分组参数,直接内联聚合逻辑,避免了Invoke节点的生成,EF就能正常解析了。
内容的提问来源于stack exchange,提问作者Aaron Vanderwielen

