无法将 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, 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
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.Buffer
1..ctor(IEnumerable1 source) at
System.Linq.Enumerable.ToArray(IEnumerable
1 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)));
否则,你必须重写它而不使用任何。
我一直在尝试将一些常见的 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.Func
2[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, 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 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.Buffer
1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray(IEnumerable
1 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)));
否则,你必须重写它而不使用任何。