无法将 Linq 表达式组合成 Func

Trouble combining Linq Expressions into a Func

我一直在尝试将一些常见的 lambda 子表达式分解为可重用的组件,但遇到了困难。我将通过一个简化的示例展示到目前为止我所做的工作,并希望你们中的任何一个能阐明一些问题。

我的子表达式最终用于 NHibernate 查询(IQueryable 接口)。这是一个例子:

var depts = session.Query<Department>().Where(d => d.employees.Any(ex1.AndAlso(ex2).Compile()));

AndAlso 是这样定义的 Expression 扩展(取自对相关 SO 问题的回答,其中一些小的调整):

public class ParameterRebinder : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> _map;

    public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
    {
        _map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
    }
    public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
    {
        return new ParameterRebinder(map).Visit(exp);
    }
    protected override Expression VisitParameter(ParameterExpression p)
    {
        ParameterExpression replacement;
        if (_map.TryGetValue(p, out replacement))
        {
            p = replacement;
        }
        return base.VisitParameter(p);
    }
}

public static class ExpressionExtensions
{
    public static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
    {
        // build parameter map (from parameters of second to parameters of first)
        var map = first.Parameters.Select((f, i) => new { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f);
        // replace parameters in the second lambda expression with parameters from the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
        // apply composition of lambda expression bodies to parameters from the first expression 
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.AndAlso);
    }
    public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.OrElse);
    }
}

除了 Any 调用是 IEnumerable 调用,而不是 IQueryable 调用,所以它期望 Func 参数而不是 Expression。为此,我在组合 Expression 上调用 Compile(),但随后出现以下 运行 时间错误:

Remotion.Linq.Parsing.ParserException: Could not parse expression 'c.employees.Any(value(System.Func2[Entities.Domain.Department,System.Boolean]))': Object of type 'System.Linq.Expressions.ConstantExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'. If you tried to pass a delegate instead of a LambdaExpression, this is not supported because delegates are not parsable expressions. at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.CreateExpressionNode(Type nodeType, MethodCallExpressionParseInfo parseInfo, Object[] additionalConstructorParameters) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable1 arguments, MethodCallExpression expressionToParse) at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier) at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot) at Remotion.Linq.Parsing.ExpressionTreeVisitors.SubQueryFindingExpressionTreeVisitor.VisitExpression(Expression expression) at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitLambdaExpression(LambdaExpression expression) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.ProcessArgumentExpression(Expression argumentExpression) at System.Linq.Enumerable.WhereSelectEnumerableIterator2.MoveNext()<br> at System.Linq.Buffer1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray(IEnumerable1 source) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse) at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier) at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot) at NHibernate.Linq.NhRelinqQueryParser.Parse(Expression expression) in NhRelinqQueryParser.cs: line 39

...堆栈的其余部分依此类推。

我的难题似乎是不能组合 Funcs,只能组合 Expressions -- 这会产生另一个 Expression。但是不能将 Expression 交给 IEnumerable.Any() -- 只有 Func。但是 Expression.Compile() 生成的 Func 似乎是错误的种类...

有什么想法吗?

迈克尔

我想到,考虑到问题围绕着 IEnumerable 方法采用 Func 个参数这一事实,如果我能得到 d.employees(在我的示例中)成为 IQueryable 而不是 IEnumerable 然后它的 Any 将采用 Expression 我就完成了。

所以,我尝试在 d.employees 之后插入一个 .AsQueryable(),并且成功了!

考虑这段代码:

Func<T1, TResult> func = t => t == 5;
Expression<Func<T1, TResult>> expression = t => t == 5;

对于此代码,func 将是对已编译代码的引用,而 expression 将是对表示 lambda 表达式的抽象语法树的引用。在表达式上调用 Compile() 将 return 编译的代码在功能上等同于 func.

编译代码中包含什么类型的表达式是无关紧要的 - LINQ 提供程序根本无法解码编译代码。他们依赖于遍历由 Expression<Func<...>> 类型表示的抽象语法树。

在您的情况下,您在 d.employees 上应用 Any()。由于 employees 是一个 IEnumerable<T>,您将获得 Any() 的版本,该版本期望获得它可以 运行 的编译代码。请注意,Any() 也可用于可查询对象,在这种情况下将接受表达式。

您可以尝试 AsQueryable(),但我不确定它是否有效:

session.Query<Department>()
       .Where(d => d.employees.AsQueryable().Any(ex1.AndAlso(ex2)));

否则,你必须重写它而不使用任何。