Combiner deux expressions (Expression <Func <T, bool >>)

249

J'ai deux expressions de type Expression<Func<T, bool>>et je veux passer à OU, ET ou NON de celles-ci et obtenir une nouvelle expression du même type

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
la source
8
Article très utile que j'ai obtenu de Google: LINQ to Entities: Combining Predicates
Thomas CG de Vilhena

Réponses:

331

Eh bien, vous pouvez utiliser Expression.AndAlso/ OrElseetc pour combiner des expressions logiques, mais le problème réside dans les paramètres; travaillez-vous avec le même ParameterExpressiondans expr1 et expr2? Si oui, c'est plus simple:

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

Cela fonctionne également bien pour annuler une seule opération:

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

Sinon, selon le fournisseur LINQ, vous pourrez peut-être les combiner avec 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;
}

Quelque part, j'ai du code qui réécrit un arbre d'expression remplaçant les nœuds pour supprimer le besoin Invoke, mais il est assez long (et je ne me souviens pas où je l'ai laissé ...)


Version généralisée qui choisit l'itinéraire le plus simple:

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

À partir de .NET 4.0, il existe la ExpressionVisitorclasse qui vous permet de créer des expressions qui sont sécurisées par 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
la source
Hé Marc, j'ai essayé ta première suggestion, dans le premier bloc de code ci-dessus, mais quand je passe l'expression "lambda" <func <T, bool >> résultant en une méthode Where, j'obtiens une erreur disant que le paramètre est hors de portée? une idée? cheers
andy
1
+1 la version généralisée fonctionne comme un charme, j'ai utilisé Et au lieu de andalso, je pensais que linq to sql ne supporte pas andalso?
Maslow
2
@Maslow - voici un réécrivain qui peut aligner les arbres pour sauver Invoke: stackoverflow.com/questions/1717444/…
Marc Gravell
1
@Aron regarde maintenant la date: le visiteur du framework .NET ( ExpressionVisitor) n'existait pas à l'époque; J'ai un exemple connexe sur stackoverflow à partir d'une date similaire où il implémente le visiteur manuellement: c'est beaucoup de code.
Marc Gravell
1
@MarkGravell, j'utilise votre première solution pour combiner mes expressions, et tout fonctionne bien même dans le cadre d'entité, alors quels seraient les avantages d'utiliser la dernière solution?
johnny 5
62

Vous pouvez utiliser Expression.AndAlso / OrElse pour combiner des expressions logiques, mais vous devez vous assurer que les ParameterExpressions sont les mêmes.

J'avais des problèmes avec EF et PredicateBuilder alors j'ai créé le mien sans recourir à Invoke, que je pourrais utiliser comme ceci:

var filterC = filterA.And(filterb);

Code source de mon 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);
    }   
}

Et la classe d'utilité pour remplacer les paramètres dans un 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
la source
Cette solution était la seule qui m'a permis d'avoir x => x.Property == Value combiné avec arg => arg.Property2 == Value. Accessoires majeurs, un peu laconique et déroutant mais ça marche donc je ne vais pas me plaindre. Félicitations Adam :-)
VulgarBinary
C'est une excellente solution.
Aaron Stainback
Adam, cela a résolu un problème très ennuyeux que j'avais avec le fournisseur Linq du modèle d'objet client SharePoint - merci de l'avoir publié.
Christopher McAtackney,
Cela a fonctionné pour moi! J'avais recherché une variété de solutions ainsi que le générateur de prédicat et rien n'a fonctionné jusqu'à présent. Je vous remercie!
tokyo0709
C'est un merveilleux morceau de code. Je n'ai pas trouvé d'endroit pour ajuster le code, copier-coller et c'est tout :)
Tolga Evcimen
19

Si votre fournisseur ne prend pas en charge Invoke et que vous devez combiner deux expressions, vous pouvez utiliser un ExpressionVisitor pour remplacer le paramètre dans la deuxième expression par le paramètre dans la première expression.

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
la source
1
Cela a résolu mon problème particulier où l'autre solution a abouti à la même exception. Merci.
Shaun Wilson
1
C'est une excellente solution.
Aaron Stainback
3

Rien de nouveau ici, mais a épousé cette réponse avec cette réponse et l'a légèrement remaniée afin que même je comprenne ce qui se passe:

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
la source
J'avais du mal à saisir le concept, et votre fusion de quelques autres réponses l'a aidé à cliquer pour moi. Merci!
Kevin M. Lapio
2

J'avais besoin d'obtenir les mêmes résultats, mais en utilisant quelque chose de plus générique (car le type n'était pas connu). Grâce à la réponse de marc, j'ai finalement compris ce que j'essayais de réaliser:

    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
la source
1

Je suggère une amélioration supplémentaire à PredicateBuilder et aux ExpressionVisitorsolutions. Je l'ai appelé UnifyParametersByNameet vous pouvez le trouver dans la bibliothèque du MIT: LinqExprHelper . Il permet de combiner des expressions lambda arbitraires. Habituellement, les questions sont posées sur l'expression des prédicats, mais cette idée s'étend également aux expressions de projection.

Le code suivant utilise une méthode ExprAdresqui crée une expression paramétrée compliquée, en utilisant lambda en ligne. Cette expression compliquée n'est codée qu'une seule fois, puis réutilisée, grâce à la LinqExprHelpermini-bibliothèque.

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

Et voici le code de construction de la sous-expression:

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

Ce que j'ai essayé de réaliser était d'exécuter des requêtes paramétrées sans avoir besoin de copier-coller et avec la possibilité d'utiliser des lambdas en ligne, qui sont si jolis. Sans tous ces trucs d'expression d'aide, je serais obligé de créer une requête entière en une seule fois.

Jarekczek
la source
-7

Je pense que cela fonctionne bien, non?

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
la source
1
cela ne peut pas être utilisé dans Linq to SQL par exemple
Romain Vergnory