Pytanie Połącz dwa wyrażenia lambda Linq


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

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

Czy istnieje sposób na utworzenie nowego wyrażenia lambda, które zasadniczo wykorzystuje wyjście fn1 i używa go jako wejścia dla fn2?

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

Wiem, że mogę utworzyć funkcję naraz, ale problem polega na tym, że robię jakiś ogólny kod i dlatego naprawdę muszę mieć możliwość tworzenia tych dwóch funkcji osobno, a następnie łączenia ich w taki sposób, aby Linq mógł ich używać na moje obiekty bazy danych (Entity Framework).


14
2017-10-17 17:24


pochodzenie


Oto odpowiedź pokazująca manipulację / kombinację ekspresji. stackoverflow.com/a/9683506/8155 - Amy B
@DavidB Dzięki za link; Nie miałem pojęcia, jak zastąpić wszystkie wystąpienia jednego wyrażenia innym. - Servy
Wielkie dzięki, chłopaki! - Peter M.


Odpowiedzi:


Logicznie rzecz biorąc, chcemy stworzyć nową lambdę, w której ma ona parametr wejścia do pierwszej funkcji, i ciało, które wywołuje pierwszą funkcję z tym parametrem, a następnie przekazuje wynik jako parametr do druga funkcja, a następnie zwraca to.

Możemy to z łatwością replikować używając Expression obiekty:

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);
}

Niestety, EF i większość innych dostawców zapytań nie będzie naprawdę wiedzieć, co z tym zrobić i nie będzie działać poprawnie. Kiedykolwiek uderzą w Invoke wyrażenie zazwyczaj po prostu rzucają pewnego rodzaju wyjątek. Trochę mogą obsłużyć to. Teoretycznie wszystkie potrzebne informacje są tam, jeśli są napisane z solidnością, aby się do nich dostać.

Możemy jednak z punktu widzenia pojęciowego zastąpić każdą instancję pierwszego parametru lambda w ciele lambda parametrem nowej lambdy, którą tworzymy, a następnie zastąpić wszystkie wystąpienia drugiego parametru lambda w drugim lambda. z nowym ciałem pierwszej lambdy. Technicznie, jeśli wyrażenia te mają efekty uboczne, a parametry te są używane więcej niż jeden raz, nie będą one takie same, ale ponieważ będą one analizowane przez dostawcę kwerend EF, nie powinny nigdy mieć skutków ubocznych.

Podziękowania dla Davida B za udostępnienie linku to powiązane pytanie który zapewnia ReplaceVisitor realizacja. Możemy tego użyć ReplaceVisitor przejść przez całe drzewo wyrażenia i zastąpić jedno wyrażenie innym. Implementacja tego typu jest:

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);
    }
}

A teraz możemy napisać naszą właściwy  Combine metoda:

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);
}

i prosty test, aby pokazać, co się dzieje:

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);

Który wydrukuje:

param => param.PossibleSubPath.MyStringProperty.Contains ("some literal")

Dokładnie tego chcemy; dostawca zapytań będzie wiedział, jak przetworzyć coś takiego.


23
2017-10-17 18:11



Gdyby fn2 używa swojego parametru więcej niż jeden raz, fn1 zostanie powtórzone więcej niż raz. Mam nadzieję, że jest to albo trywialne, albo jest obsługiwane inteligentnie przez dostawcę zapytań, ale może to w końcu doprowadzić do znacznej pracy wykonanej wiele razy (coś takiego jak zmienna lokalna powinna zadziałać, chyba że dostawcy zapytań prawdopodobnie by tego nie poparli). - Tim S.
@TimS. Tak, omówiłem to trochę w mojej odpowiedzi. Szczerze mówiąc, nie oczekuję, że będzie to poważny problem, ponieważ oczekiwałbym, że optymalizator zapytań na końcu DB będzie w stanie skutecznie poradzić sobie z takimi przypadkami. Wydaje się również, że rodzaj wyrażeń, na które patrzy PO, nie stanowiłby problemu. Trudno powiedzieć. Lokalna zmienna prawie na pewno nie byłaby w stanie zostać poprawnie przeanalizowana, jak wspomniałeś. - Servy
@Servy, czy możesz zaktualizować swoją odpowiedź, aby odzwierciedlić, Przekazanie parametru z pierwszego ciała nie zadziała, ponieważ ten parametr jest zdefiniowany w zakresie oddzielnego wyrażenia, więc po połączeniu wyrażenia x => x.condition == true && x.anotherCondition == false drugi x nie jest taki sam x w pierwszym. (Możesz prawdopodobnie powiedzieć, co staram się powiedzieć lepiej) - johnny 5