web-dev-qa-db-fra.com

Combinaison de deux expressions (Expression <Func <T, bool >>)

J'ai deux expressions de type Expression<Func<T, bool>> et je veux prendre OR, AND ou NOT 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
227
BjartN

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

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 un code qui réécrit une arborescence d'expression remplaçant des nœuds pour supprimer le besoin de Invoke, mais il est assez long (et je ne me souviens plus 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 classe ExpressionVistor qui vous permet de créer des expressions qui sont sûres avec 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);
        }
    }
298
Marc Gravell

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

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

var filterC = filterA.And(filterb);

Code source pour 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 substituer 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;
        }
    }
53
Adam Tegen

Joe Albahari (auteur de C # 3.0 en bref et LINQPad) a écrit un utilitaire appelé PredicateBuilder qui peut être utilisé pour AND et OR fonctionne ensemble.

http://www.albahari.com/nutshell/predicatebuilder.aspx

Bien que cela fonctionne sur des fonctions, il est open source afin que vous puissiez le vérifier et voir comment cela fonctionne.

30
Cameron MacFarland

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 de la deuxième expression par le paramètre de la première.

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" }));
}
17
Francis

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

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

Je suggère encore une amélioration aux solutions PredicateBuilder et ExpressionVisitor. Je l'ai appelé UnifyParametersByName et vous pouvez le trouver dans MIT bibliothèque de la mienne: LinqExprHelper . Cela permet de combiner des expressions lambda arbitraires. Habituellement, les questions sont question sur l’expression de prédicat, mais cette idée s’applique également aux expressions de projection.

Le code suivant utilise une méthode ExprAdres qui crée une expression paramétrée complexe à l'aide de la méthode lambda inline. Cette expression compliquée n'est codée qu'une fois, puis réutilisée, grâce à la mini-bibliothèque LinqExprHelper.

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 bâtiment 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 faire, c’est d’effectuer des requêtes paramétrées sans avoir à copier-coller et à utiliser des lambdas en ligne, qui sont si jolis. Sans toutes ces expressions d'aide, je serais obligé de créer une requête entière en une fois.

1
Jarekczek