所以从逻辑上讲,我们想要做的是创建一个新的 lambda,其中它具有第一个函数的输入参数,以及一个使用该参数调用第一个函数的主体,然后将结果作为参数传递给第二个函数,然后返回它。
我们可以使用Expression
对象轻松地复制它:
public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
Expression<Func<T1, T2>> first,
Expression<Func<T2, T3>> second)
{
var param = Expression.Parameter(typeof(T1), "param");
var body = Expression.Invoke(second, Expression.Invoke(first, param));
return Expression.Lambda<Func<T1, T3>>(body, param);
}
可悲的是,EF 和大多数其他查询提供程序并不真正知道如何处理它并且无法正常运行。每当他们遇到一个Invoke
表达式时,他们通常只是抛出某种异常。不过有些人可以应付。从理论上讲,他们需要的所有信息都在那里,如果他们写得足够健壮的话。
然而,我们可以做的是,从概念的角度来看,用我们正在创建的新 lambda 的参数替换该 lambda 主体中第一个 lambda 参数的每个实例,然后替换第二个 lambda 中第二个 lambda 参数的所有实例使用第一个 lambda 的新主体。从技术上讲,如果这些表达式有副作用,并且这些参数被多次使用,它们就不会相同,但由于这些将由 EF 查询提供程序解析,它们真的不应该有副作用。
感谢 David B 提供了指向这个相关问题的链接,该问题提供了一个ReplaceVisitor
实现。我们可以使用它ReplaceVisitor
来遍历表达式的整个树并用另一个表达式替换一个表达式。该类型的实现是:
class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
现在我们可以编写正确的 Combine
方法了:
public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
this Expression<Func<T1, T2>> first,
Expression<Func<T2, T3>> second)
{
var param = Expression.Parameter(typeof(T1), "param");
var newFirst = new ReplaceVisitor(first.Parameters.First(), param)
.Visit(first.Body);
var newSecond = new ReplaceVisitor(second.Parameters.First(), newFirst)
.Visit(second.Body);
return Expression.Lambda<Func<T1, T3>>(newSecond, param);
}
和一个简单的测试用例,只是为了演示发生了什么:
Expression<Func<MyObject, string>> fn1 = x => x.PossibleSubPath.MyStringProperty;
Expression<Func<string, bool>> fn2 = x => x.Contains("some literal");
var composite = fn1.Combine(fn2);
Console.WriteLine(composite);
这将打印出:
param => param.PossibleSubPath.MyStringProperty.Contains("some literal")
这正是我们想要的;查询提供者将知道如何解析类似的东西。