Łączenie dwóch wyrażeń (wyrażenie <Func <T, bool >>)

249

Mam dwa wyrażenia typu Expression<Func<T, bool>>i chcę przyjąć OR, AND lub NOT z nich i uzyskać nowe wyrażenie tego samego typu

Expression<Func<T, bool>> expr1;
Expression<Func<T, bool>> expr2;

...

//how to do this (the code below will obviously not work)
Expression<Func<T, bool>> andExpression = expr AND expr2
BjartN
źródło
8
Bardzo przydatny post, który dostałem od Google: LINQ do Entities: Combining Predicates
Thomas CG de Vilhena

Odpowiedzi:

331

Cóż, możesz użyć Expression.AndAlso/ OrElseetc do połączenia wyrażeń logicznych, ale problemem są parametry; pracujesz z tym samym ParameterExpressionw expr1 i expr2? Jeśli tak, to łatwiej:

var body = Expression.AndAlso(expr1.Body, expr2.Body);
var lambda = Expression.Lambda<Func<T,bool>>(body, expr1.Parameters[0]);

Działa to również dobrze, aby negować pojedynczą operację:

static Expression<Func<T, bool>> Not<T>(
    this Expression<Func<T, bool>> expr)
{
    return Expression.Lambda<Func<T, bool>>(
        Expression.Not(expr.Body), expr.Parameters[0]);
}

W przeciwnym razie, w zależności od dostawcy LINQ, możesz być w stanie połączyć je z Invoke:

// OrElse is very similar...
static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> left,
    Expression<Func<T, bool>> right)
{
    var param = Expression.Parameter(typeof(T), "x");
    var body = Expression.AndAlso(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param)
        );
    var lambda = Expression.Lambda<Func<T, bool>>(body, param);
    return lambda;
}

Gdzieś mam kod, który ponownie zapisuje drzewo wyrażeń zastępujące węzły, aby usunąć potrzebę Invoke, ale jest dość długi (i nie pamiętam, gdzie go zostawiłem ...)


Uogólniona wersja, która wybiera najprostszą trasę:

static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> expr1,
    Expression<Func<T, bool>> expr2)
{
    // need to detect whether they use the same
    // parameter instance; if not, they need fixing
    ParameterExpression param = expr1.Parameters[0];
    if (ReferenceEquals(param, expr2.Parameters[0]))
    {
        // simple version
        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(expr1.Body, expr2.Body), param);
    }
    // otherwise, keep expr1 "as is" and invoke expr2
    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(
            expr1.Body,
            Expression.Invoke(expr2, param)), param);
}

Począwszy od .NET 4.0 istnieje ExpressionVisitorklasa, która pozwala budować wyrażenia bezpieczne dla EF.

    public static Expression<Func<T, bool>> AndAlso<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var parameter = Expression.Parameter(typeof (T));

        var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);

        var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);

        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(left, right), parameter);
    }



    private class ReplaceExpressionVisitor
        : ExpressionVisitor
    {
        private readonly Expression _oldValue;
        private readonly Expression _newValue;

        public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
        {
            _oldValue = oldValue;
            _newValue = newValue;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldValue)
                return _newValue;
            return base.Visit(node);
        }
    }
Marc Gravell
źródło
Hej Marc, wypróbowałem twoją pierwszą sugestię, w twoim pierwszym bloku kodu powyżej, ale kiedy przekazuję wyrażenie "lambda" <func <T, bool >> w wyniku metody Where, pojawia się błąd informujący, że parametr to poza zakresem? dowolny pomysł? na zdrowie
andy
1
+1 uogólniona wersja działa jak urok, użyłem I zamiast andalso, myślałem, że linq to sql nie obsługuje andalso?
Masłów
2
@Maslow - oto przepisarka, która może wstawić drzewa, aby uratować Invoke: stackoverflow.com/questions/1717444/…
Marc Gravell
1
@Aron spogląda teraz na datę: wtedy gości .NET (.NET Framework ExpressionVisitor) nie istniał wtedy; Mam podobny przykład dotyczący przepełnienia stosu z podobnej daty, w której ręcznie implementuje on gościa: jest to dużo kodu.
Marc Gravell
1
@MarkGravell, używam twojego pierwszego rozwiązania do połączenia moich wyrażeń i wszystko działa dobrze nawet w encjiframework, więc jakie byłyby korzyści z zastosowania ostatniego rozwiązania?
Johnny 5
62

Możesz użyć Expression.AndAlso / OrElse, aby połączyć wyrażenia logiczne, ale musisz upewnić się, że ParameterExpressions są takie same.

Miałem problemy z EF i PredicateBuilder, więc stworzyłem własny bez uciekania się do Invoke, którego mógłbym użyć w następujący sposób:

var filterC = filterA.And(filterb);

Kod źródłowy mojego PredicateBuilder:

public static class PredicateBuilder {

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }   
}

I klasa użyteczności do zastąpienia parametrów w lambda:

internal class SubstExpressionVisitor : System.Linq.Expressions.ExpressionVisitor {
        public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

        protected override Expression VisitParameter(ParameterExpression node) {
            Expression newValue;
            if (subst.TryGetValue(node, out newValue)) {
                return newValue;
            }
            return node;
        }
    }
Adam Tegen
źródło
To rozwiązanie było jedynym, które pozwoliło mi mieć x => x.Property == Wartość w połączeniu z arg => arg.Property2 == Wartość. Duże rekwizyty, trochę zwięzłe i mylące, ale działa, więc nie zamierzam narzekać. Kudos Adam :-)
VulgarBinary
To świetne rozwiązanie.
Aaron Stainback
Adam, rozwiązałem bardzo irytujący problem, który miałem przy użyciu dostawcy Linq modelu klienta SharePoint Client Object - dzięki za opublikowanie go.
Christopher McAtackney
To zadziałało dla mnie! Szukałem różnych rozwiązań, a także konstruktora predykatów i do tego czasu nic nie działało. Dziękuję Ci!
tokyo0709
To wspaniały kawałek kodu. Nie mogłem znaleźć miejsca na dostosowanie kodu, skopiuj-wklej i to wszystko :)
Tolga Evcimen
19

Jeśli dostawca nie obsługuje Invoke i konieczne jest połączenie dwóch wyrażeń, można użyć ExpressionVisitor, aby zastąpić parametr w drugim wyrażeniu parametrem w pierwszym wyrażeniu.

class ParameterUpdateVisitor : ExpressionVisitor
{
    private ParameterExpression _oldParameter;
    private ParameterExpression _newParameter;

    public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (object.ReferenceEquals(node, _oldParameter))
            return _newParameter;

        return base.VisitParameter(node);
    }
}

static Expression<Func<T, bool>> UpdateParameter<T>(
    Expression<Func<T, bool>> expr,
    ParameterExpression newParameter)
{
    var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
    var body = visitor.Visit(expr.Body);

    return Expression.Lambda<Func<T, bool>>(body, newParameter);
}

[TestMethod]
public void ExpressionText()
{
    string text = "test";

    Expression<Func<Coco, bool>> expr1 = p => p.Item1.Contains(text);
    Expression<Func<Coco, bool>> expr2 = q => q.Item2.Contains(text);
    Expression<Func<Coco, bool>> expr3 = UpdateParameter(expr2, expr1.Parameters[0]);

    var expr4 = Expression.Lambda<Func<Recording, bool>>(
        Expression.OrElse(expr1.Body, expr3.Body), expr1.Parameters[0]);

    var func = expr4.Compile();

    Assert.IsTrue(func(new Coco { Item1 = "caca", Item2 = "test pipi" }));
}
Francis
źródło
1
To rozwiązało mój szczególny problem, gdy inne rozwiązanie spowodowało ten sam wyjątek. Dzięki.
Shaun Wilson,
1
To świetne rozwiązanie.
Aaron Stainback
3

Nic nowego tutaj, ale połączyłem tę odpowiedź z tą odpowiedzią i nieco ją przeredagowałem, aby nawet ja rozumiałem, co się dzieje:

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        ParameterExpression parameter1 = expr1.Parameters[0];
        var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
        var body2WithParam1 = visitor.Visit(expr2.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private ParameterExpression _oldParameter;
        private ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (ReferenceEquals(node, _oldParameter))
                return _newParameter;

            return base.VisitParameter(node);
        }
    }
}
Dejan
źródło
Miałem trudności z uchwyceniem koncepcji, a połączenie kilku innych odpowiedzi pomogło mi ją kliknąć. Dzięki!
Kevin M. Lapio
2

Musiałem osiągnąć te same wyniki, ale używając czegoś bardziej ogólnego (ponieważ typ nie był znany). Dzięki odpowiedzi Marca w końcu zrozumiałem, co próbowałem osiągnąć:

    public static LambdaExpression CombineOr(Type sourceType, LambdaExpression exp, LambdaExpression newExp) 
    {
        var parameter = Expression.Parameter(sourceType);

        var leftVisitor = new ReplaceExpressionVisitor(exp.Parameters[0], parameter);
        var left = leftVisitor.Visit(exp.Body);

        var rightVisitor = new ReplaceExpressionVisitor(newExp.Parameters[0], parameter);
        var right = rightVisitor.Visit(newExp.Body);

        var delegateType = typeof(Func<,>).MakeGenericType(sourceType, typeof(bool));
        return Expression.Lambda(delegateType, Expression.Or(left, right), parameter);
    }
VorTechS
źródło
1

Sugeruję jeszcze jedną poprawę w PredicateBuilder i ExpressionVisitorrozwiązaniach. Zadzwoniłem UnifyParametersByNamei można go znaleźć w mojej bibliotece MIT: LinqExprHelper . Pozwala łączyć dowolne wyrażenia lambda. Zwykle zadawane są pytania dotyczące wyrażenia predykatowego, ale ta idea obejmuje również wyrażenia projekcyjne.

Poniższy kod wykorzystuje metodę, ExprAdresktóra tworzy skomplikowane sparametryzowane wyrażenie, używając wbudowanej lambda. To skomplikowane wyrażenie jest kodowane tylko raz, a następnie ponownie używane, dzięki LinqExprHelpermini-bibliotece.

public IQueryable<UbezpExt> UbezpFull
{
    get
    {
        System.Linq.Expressions.Expression<
            Func<UBEZPIECZONY, UBEZP_ADRES, UBEZP_ADRES, UbezpExt>> expr =
            (u, parAdrM, parAdrZ) => new UbezpExt
            {
                Ub = u,
                AdrM = parAdrM,
                AdrZ = parAdrZ,
            };

        // From here an expression builder ExprAdres is called.
        var expr2 = expr
            .ReplacePar("parAdrM", ExprAdres("M").Body)
            .ReplacePar("parAdrZ", ExprAdres("Z").Body);
        return UBEZPIECZONY.Select((Expression<Func<UBEZPIECZONY, UbezpExt>>)expr2);
    }
}

A to jest kod budujący podwyrażenie:

public static Expression<Func<UBEZPIECZONY, UBEZP_ADRES>> ExprAdres(string sTyp)
{
    return u => u.UBEZP_ADRES.Where(a => a.TYP_ADRESU == sTyp)
        .OrderByDescending(a => a.DATAOD).FirstOrDefault();
}

To, co próbowałem osiągnąć, to wykonywanie sparametryzowanych zapytań bez potrzeby kopiowania i wklejania oraz możliwość korzystania z wbudowanych lambd, które są tak ładne. Bez tych wszystkich wyrażeń pomocniczych byłbym zmuszony stworzyć całe zapytanie za jednym razem.

Jarekczek
źródło
-7

Myślę, że to działa dobrze, prawda?

Func<T, bool> expr1 = (x => x.Att1 == "a");
Func<T, bool> expr2 = (x => x.Att2 == "b");
Func<T, bool> expr1ANDexpr2 = (x => expr1(x) && expr2(x));
Func<T, bool> expr1ORexpr2 = (x => expr1(x) || expr2(x));
Func<T, bool> NOTexpr1 = (x => !expr1(x));
Céline
źródło
1
nie można tego użyć na przykład w Linq do SQL
Romain Vergnory,