Объедините два лямбда-выражения Linq - программирование
Подтвердить что ты не робот

Объедините два лямбда-выражения Linq

Expression<Func<MyObject, string>> fn1 = x => x.PossibleSubPath.MyStringProperty;

Expression<Func<string, bool>> fn2 = x => x.Contains("some literal");

Есть ли способ создать новое лямбда-выражение, которое в основном использует вывод fn1 и использует его как вход для fn2?

Expression<Func<MyObject, bool>> fnCombined = ...

Я знаю, что я могу создать функцию сразу, но проблема в том, что я делаю некоторый общий код и поэтому действительно должен иметь возможность создавать эти две функции отдельно, а затем объединять их таким образом, чтобы Linq мог используйте их в моих объектах базы данных (Entity Framework).

4b9b3361

Ответ 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" )

Это именно то, что мы хотим; поставщик запроса будет знать, как разбирать что-то подобное.