Kombinieren von zwei Ausdrücken (Ausdruck <Func <T, bool >>)

249

Ich habe zwei Typausdrücke Expression<Func<T, bool>>und möchte zu OR, AND oder NOT von diesen wechseln und einen neuen Ausdruck desselben Typs erhalten

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
quelle
8
Sehr nützlicher Beitrag, den ich von Google erhalten habe: LINQ zu Entitäten: Prädikate kombinieren
Thomas CG de Vilhena

Antworten:

331

Nun, Sie können Expression.AndAlso/ OrElseetc verwenden, um logische Ausdrücke zu kombinieren, aber das Problem sind die Parameter; ParameterExpressionarbeitest du mit dem gleichen in expr1 und expr2? Wenn ja, ist es einfacher:

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

Dies funktioniert auch gut, um eine einzelne Operation zu negieren:

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

Andernfalls können Sie sie je nach LINQ-Anbieter möglicherweise kombinieren mit 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;
}

Irgendwo habe ich Code, der einen Ausdrucksbaum neu schreibt, der Knoten ersetzt, um die Notwendigkeit zu beseitigen Invoke, aber er ist ziemlich lang (und ich kann mich nicht erinnern, wo ich ihn gelassen habe ...)


Verallgemeinerte Version, die den einfachsten Weg wählt:

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

Ab .NET 4.0 gibt es die ExpressionVisitorKlasse, mit der Sie Ausdrücke erstellen können, die EF-sicher sind.

    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
quelle
Hey Marc, ich habe deinen ersten Vorschlag in deinem ersten Codeblock oben ausprobiert, aber wenn ich den "Lambda" -Ausdruck <func <T, bool >> übergebe, erhalte ich eine Fehlermeldung, dass der Parameter lautet außer Reichweite? irgendeine Idee? Prost
Andy
1
+1 Die verallgemeinerte Version funktioniert wie ein Zauber, den ich verwendet habe. Und anstelle von andalso dachte ich, dass linq to sql andalso nicht unterstützt.
Maslow
2
@ Maslow - hier ist ein Umschreiber, der die Bäume inline einfügen kann, um Invoke zu speichern: stackoverflow.com/questions/1717444/…
Marc Gravell
1
@Aron schauen Sie sich jetzt das Datum an: Der .NET Framework-Besucher ( ExpressionVisitor) existierte damals noch nicht ; Ich habe ein verwandtes Beispiel für den Stapelüberlauf ab einem ähnlichen Datum, bei dem der Besucher manuell implementiert wird: Es ist viel Code.
Marc Gravell
1
@MarkGravell, ich verwende Ihre erste Lösung, um meine Ausdrücke zu kombinieren, und alles funktioniert auch im Entityframework einwandfrei. Welche Vorteile hätte die Verwendung der letzten Lösung?
Johnny 5.
61

Sie können Expression.AndAlso / OrElse verwenden, um logische Ausdrücke zu kombinieren. Sie müssen jedoch sicherstellen, dass die ParameterExpressions identisch sind.

Ich hatte Probleme mit EF und dem PredicateBuilder, also habe ich meine eigenen gemacht, ohne auf Invoke zurückzugreifen, die ich so verwenden könnte:

var filterC = filterA.And(filterb);

Quellcode für meinen 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);
    }   
}

Und die Utility-Klasse zum Ersetzen der Parameter in einem 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
quelle
Diese Lösung war die einzige, die es mir ermöglichte, x => x.Property == Value mit arg => arg.Property2 == Value zu kombinieren. Wichtige Requisiten, etwas knapp und verwirrend, aber es funktioniert, also werde ich mich nicht beschweren. Kudos Adam :-)
VulgarBinary
Dies ist eine großartige Lösung.
Aaron Stainback
Adam, dies löste ein sehr ärgerliches Problem, das ich mit dem Linq-Anbieter des SharePoint Client Object-Modells hatte - danke, dass Sie es veröffentlicht haben.
Christopher McAtackney
Das hat bei mir funktioniert! Ich hatte nach einer Vielzahl von Lösungen sowie nach Prädikaten-Buildern gesucht und nichts funktionierte bis dahin. Danke dir!
tokyo0709
Dies ist ein wunderbarer Code. Ich konnte keinen Platz finden, um den Code anzupassen, zu kopieren und einzufügen und das war's :)
einzufügen Tolga Evcimen
19

Wenn Ihr Provider Invoke nicht unterstützt und Sie zwei Ausdrücke kombinieren müssen, können Sie einen ExpressionVisitor verwenden, um den Parameter im zweiten Ausdruck durch den Parameter im ersten Ausdruck zu ersetzen.

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
quelle
1
Dies löste mein spezielles Problem, bei dem die andere Lösung zu derselben Ausnahme führte. Vielen Dank.
Shaun Wilson
1
Dies ist eine großartige Lösung.
Aaron Stainback
3

Nichts Neues hier, aber diese Antwort mit dieser Antwort verheiratet und leicht überarbeitet, so dass selbst ich verstehe, was los ist:

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
quelle
Ich hatte Schwierigkeiten, das Konzept zu verstehen, und Ihre Verschmelzung einiger anderer Antworten hat mir geholfen, darauf zu klicken. Vielen Dank!
Kevin M. Lapio
2

Ich musste die gleichen Ergebnisse erzielen, aber etwas allgemeineres verwenden (da der Typ nicht bekannt war). Dank Marc's Antwort fand ich endlich heraus, was ich erreichen wollte:

    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
quelle
1

Ich schlage eine weitere Verbesserung von PredicateBuilder und ExpressionVisitorLösungen vor. Ich habe es angerufen UnifyParametersByNameund Sie finden es in meiner MIT-Bibliothek: LinqExprHelper . Es ermöglicht das Kombinieren von willkürlichen Lambda-Ausdrücken. Normalerweise werden die Fragen zum Prädikatenausdruck gestellt, aber diese Idee erstreckt sich auch auf Projektionsausdrücke.

Der folgende Code verwendet eine Methode, ExprAdresdie mithilfe von Inline-Lambda einen komplizierten parametrisierten Ausdruck erstellt. Dieser komplizierte Ausdruck wird nur einmal codiert und dank der LinqExprHelperMinibibliothek wiederverwendet .

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

Und dies ist der Code für die Erstellung von Unterausdrücken:

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

Was ich erreichen wollte, war, parametrisierte Abfragen durchzuführen, ohne kopieren und einfügen zu müssen und mit der Fähigkeit, Inline-Lambdas zu verwenden, die so hübsch sind. Ohne all diese Hilfsmittel wäre ich gezwungen, eine ganze Abfrage auf einmal zu erstellen.

Jarekczek
quelle
-7

Ich denke, das funktioniert gut, nicht wahr?

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
quelle
1
Dies kann zum Beispiel nicht in Linq to SQL verwendet werden
Romain Vergnory