将返回IEnumerable的动态LINQ筛选函数改为返回IQueryable
首先,你的核心思路完全正确:应该返回IQueryable<T>而非IEnumerable<T>,这样就能把筛选和排序的表达式树组合起来,最后一次性执行查询,避免中途编译表达式导致的内存操作(IEnumerable的Where是在内存中过滤,而IQueryable的Where会把表达式传递给数据源执行,比如EF Core会转成SQL)。
你遇到的报错,主要是因为表达式树构建过程中存在几个细节错误,下面一步步解决:
1. 修复表达式树构建的错误
1.1 EndsWith方法调用参数缺失
你在ExpressionRetriever中获取的endsWithMethod是接受两个参数(string和StringComparison)的重载,但在case Comparison.EndsWith时只传了constant,少了comparisonType参数,这会导致表达式树无效,运行时抛出异常。修改如下:
case Comparison.EndsWith: return Expression.Call(member, endsWithMethod, constant, comparisonType);
1.2 BoolTest可以简化为Expression.Equal
你当前用booleanEqualMethod来调用Equals方法,其实直接用Expression.Equal更简洁,也避免了方法获取可能的问题:
case Comparison.BoolTest: return Expression.Equal(member, Expression.Constant(Convert.ToBoolean(filter.Value), typeof(bool)));
1.3 处理类型不匹配的情况
如果filter.Value的类型和属性类型不一致(比如属性是int,但Value是string),直接构建表达式会报错,需要添加类型转换:
ConstantExpression constant = Expression.Constant(Convert.ChangeType(filter.Value, member.Type), member.Type);
1.4 空筛选列表的处理
当前代码在filters.Count == 0时返回null,这会导致后续排序调用空引用,应该返回原数据源:
if (filters.Count == 0) return source;
2. 修改ConstructAndExpressionTree返回IQueryable<T>
去掉Compile()调用,直接返回IQueryable<T>,这样就能和排序的表达式树组合:
public static IQueryable<T> ConstructAndExpressionTree<T>(IQueryable<T> source, List<ExpressionFilter> filters) { if (filters.Count == 0) return source; ParameterExpression param = Expression.Parameter(typeof(T), "t"); Expression exp = ExpressionRetriever.GetExpression<T>(param, filters[0]); for (int i = 1; i < filters.Count; i++) { exp = Expression.And(exp, ExpressionRetriever.GetExpression<T>(param, filters[i])); } var lambda = Expression.Lambda<Func<T, bool>>(exp, param); return source.Where(lambda); }
3. 完整修改后的ExpressionRetriever
public static class ExpressionRetriever { private static MethodInfo containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string), typeof(StringComparison) }); private static MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string), typeof(StringComparison) }); private static MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new Type[] { typeof(string), typeof(StringComparison) }); private static MethodInfo indexOfMethod = typeof(string).GetMethod("IndexOf", new Type[] { typeof(string), typeof(StringComparison) }); public static Expression GetExpression<T>(ParameterExpression param, ExpressionFilter filter) { MemberExpression member = Expression.Property(param, filter.PropertyName); // 处理类型转换,确保Value和属性类型匹配 var convertedValue = Convert.ChangeType(filter.Value, member.Type); ConstantExpression constant = Expression.Constant(convertedValue, member.Type); ConstantExpression comparisonType = Expression.Constant(StringComparison.OrdinalIgnoreCase); switch (filter.Comparison) { case Comparison.Equal: return Expression.Equal(member, constant); case Comparison.GreaterThan: return Expression.GreaterThan(member, constant); case Comparison.GreaterThanOrEqual: return Expression.GreaterThanOrEqual(member, constant); case Comparison.LessThan: return Expression.LessThan(member, constant); case Comparison.LessThanOrEqual: return Expression.LessThanOrEqual(member, constant); case Comparison.NotEqual: return Expression.NotEqual(member, constant); case Comparison.Contains: return Expression.Call(member, containsMethod, constant, comparisonType); case Comparison.StartsWith: return Expression.Call(member, startsWithMethod, constant, comparisonType); case Comparison.IndexOf: return Expression.NotEqual( Expression.Call(member, indexOfMethod, constant, comparisonType), Expression.Constant(-1, typeof(int)) ); case Comparison.EndsWith: return Expression.Call(member, endsWithMethod, constant, comparisonType); case Comparison.BoolTest: return Expression.Equal(member, Expression.Constant(Convert.ToBoolean(filter.Value), typeof(bool))); default: throw new ArgumentOutOfRangeException(nameof(filter.Comparison), filter.Comparison, null); } } }
4. 组合筛选和排序的用法
现在你可以先调用筛选,再调用排序,最后执行查询(比如用ToList()):
// 示例:构建筛选条件 var filters = new List<ExpressionFilter> { new ExpressionFilter { PropertyName = "Age", Value = 30, Comparison = Comparison.GreaterThan }, new ExpressionFilter { PropertyName = "Name", Value = "J", Comparison = Comparison.StartsWith } }; // 构建排序条件 var sortItems = new List<KeyValuePair<string, SortDirection>> { new KeyValuePair<string, SortDirection>("Age", SortDirection.desc), new KeyValuePair<string, SortDirection>("Name", SortDirection.asc) }; // 组合筛选和排序,最后执行 var query = DynamicFilteringHelper.ConstructAndExpressionTree(dbContext.Users.AsQueryable(), filters); var sortedQuery = DynamicOrderingHelper.OrderByAndThenBy(query, sortItems); var result = sortedQuery.ToList(); // 此时才会编译并执行表达式树
为什么之前的尝试报错?
除了上述的参数缺失问题,当你直接调用source.Where(Expression.Lambda<Func<T, bool>>(exp, param))时,如果表达式树本身有错误(比如EndsWith参数不足),就会抛出"Operation is not valid due to the current state of the object"异常,这是表达式树构建无效导致的运行时错误,而非IQueryable本身的问题。
现在修改后,表达式树是完整有效的,就能正常和排序的表达式树组合,最后一次性执行查询,充分利用IQueryable的延迟执行特性。
内容的提问来源于stack exchange,提问作者si2030

