Ответ 1
Таким образом, логически то, что мы хотим сделать, это создать новую лямбда, в которой он имеет параметр ввода для первой функции, и тело, которое вызывает первую функцию с этим параметром, а затем передает результат как параметр для второй функции, а затем возвращает это.
Мы можем реплицировать это достаточно легко, используя объекты 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
, они обычно просто бросают какое-то исключение. Некоторые могут справиться с этим, хотя. Теоретически вся информация, в которой они нуждаются, есть, если они написаны с надежностью, чтобы добраться до нее.
Однако мы можем с концептуальной точки зрения заменить каждый экземпляр первого лямбда-параметра в этом лямбда-теле с параметром новой лямбда, которую мы создаем, а затем заменить все экземпляры второго лямбда-параметра в вторая лямбда с новым телом первой лямбды. Технически, если эти выражения имеют побочные эффекты, и эти параметры используются более одного раза, они не будут одинаковыми, но поскольку они будут проанализированы поставщиком запросов EF, у них действительно не должно быть побочных эффектов.
Благодаря Дэвиду Б. за предоставление ссылки на этот связанный вопрос, который обеспечивает реализацию 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>(
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 = Combine(fn1, fn2);
Console.WriteLine(composite);
Что будет печататься:
param = > param.PossibleSubPath.MyStringProperty.Contains( "some literal" )
Это именно то, что мы хотим; поставщик запроса будет знать, как разбирать что-то подобное.