Combinazione di due espressioni (Espressione <Func <T, bool >>)


249

Ho due espressioni di tipo Expression<Func<T, bool>>e voglio prendere OR, AND o NOT di questi e ottenere una nuova espressione dello stesso tipo

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

8
Post molto utile che ho ricevuto da Google: LINQ to Entities: Combining Predicates
Thomas CG de Vilhena

Risposte:


331

Bene, puoi usare Expression.AndAlso/ OrElseetc per combinare espressioni logiche, ma il problema sono i parametri; stai lavorando con lo stesso ParameterExpressionin expr1 ed expr2? In tal caso, è più semplice:

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

Questo funziona bene anche per annullare una singola operazione:

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

Altrimenti, a seconda del provider LINQ, potresti essere in grado di combinarli con 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;
}

Da qualche parte, ho del codice che riscrive un albero delle espressioni sostituendo i nodi per rimuovere la necessità Invoke, ma è piuttosto lungo (e non ricordo dove l'ho lasciato ...)


Versione generalizzata che seleziona il percorso più semplice:

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

A partire da .NET 4.0, esiste la ExpressionVisitorclasse che consente di creare espressioni che sono EF sicure.

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

Ehi Marc, ho provato il tuo primo suggerimento, nel tuo primo blocco di codice sopra, ma quando passo l'espressione "lambda" <func <T, bool >> risulta in un metodo Where, ricevo un errore dicendo che il parametro è fuori portata? qualche idea? salute
andy,

1
+1 la versione generalizzata funziona come un incantesimo, ho usato E invece di andalso, ho pensato che linq to sql non supportasse andalso?
Maslow,

2
@Maslow - ecco una masterizzatore in grado di inline gli alberi per salvare Invoke: stackoverflow.com/questions/1717444/...
Marc Gravell

1
@Aron ora guarda la data: il visitatore .NET framework ( ExpressionVisitor) non esisteva allora; Ho un esempio correlato su StackOverflow da una data simile in cui implementa manualmente il visitatore: è un sacco di codice.
Marc Gravell

1
@MarkGravell, sto usando la tua prima soluzione per combinare le mie espressioni, e tutto funziona perfettamente anche nel lavoro con le entità, quindi quali sarebbero i vantaggi dell'utilizzo dell'ultima soluzione?
johnny 5

62

È possibile utilizzare Expression.AndAlso / OrElse per combinare espressioni logiche, ma è necessario assicurarsi che ParameterExpressions siano le stesse.

Ho avuto problemi con EF e PredicateBuilder, quindi ho creato il mio senza ricorrere a Invoke, che avrei potuto usare in questo modo:

var filterC = filterA.And(filterb);

Codice sorgente per il mio 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);
    }   
}

E la classe di utilità per sostituire i parametri in 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;
        }
    }

Questa soluzione è stata l'unica che mi ha permesso di avere x => x.Property == Value combinato con arg => arg.Property2 == Value. Grandi oggetti di scena, un po 'concisi e confusi, ma funziona, quindi non mi lamento. Kudos Adam :-)
VulgarBinary

Questa è un'ottima soluzione
Aaron Stainback,

Adam, questo ha risolto un fastidioso problema che stavo avendo utilizzando il provider Linq del modello di oggetti client di SharePoint, grazie per averlo pubblicato.
Christopher McAtackney,

Questo ha funzionato per me! Avevo cercato una varietà di soluzioni e un costruttore di predicati e nulla ha funzionato fino a questo. Grazie!
tokyo0709,

Questo è un meraviglioso pezzo di codice. Non sono riuscito a trovare un posto per modificare il codice, copia-incolla e basta :)
Tolga Evcimen,

19

Se il provider non supporta Invoke e è necessario combinare due espressioni, è possibile utilizzare un ExpressionVisitor per sostituire il parametro nella seconda espressione con il parametro nella prima espressione.

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

1
Ciò ha risolto il mio problema particolare in cui l'altra soluzione ha comportato la stessa eccezione. Grazie.
Shaun Wilson

1
Questa è un'ottima soluzione
Aaron Stainback,

3

Niente di nuovo qui, ma ho sposato questa risposta con questa risposta e l'ho leggermente riformattata in modo che anche io capisca cosa sta succedendo:

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

Avevo difficoltà a comprendere il concetto e la tua fusione di un paio di altre risposte mi ha aiutato a fare clic per me. Grazie!
Kevin M. Lapio,

2

Avevo bisogno di ottenere gli stessi risultati, ma usando qualcosa di più generico (dato che il tipo non era noto). Grazie alla risposta di Marc ho finalmente capito cosa stavo cercando di ottenere:

    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

Suggerisco un ulteriore miglioramento a PredicateBuilder e alle ExpressionVisitorsoluzioni. L'ho chiamato UnifyParametersByNamee lo puoi trovare nella mia libreria MIT: LinqExprHelper . Permette di combinare espressioni arbitrarie lambda. Di solito vengono poste domande sull'espressione predicata, ma questa idea si estende anche alle espressioni di proiezione.

Il codice seguente utilizza un metodo ExprAdresche crea un'espressione parametrica complicata, usando lambda inline. Questa espressione complicata viene codificata una sola volta e quindi riutilizzata, grazie alla LinqExprHelpermini-libreria.

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

E questo è il codice di costruzione della sottoespressione:

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

Quello che ho cercato di ottenere è stato quello di eseguire query parametrizzate senza bisogno di copiare e incollare e con la possibilità di utilizzare lambda in linea, che sono così carini. Senza tutte queste cose di espressione di aiuto, sarei costretto a creare un'intera query in una volta sola.


-7

Penso che funzioni bene, no?

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

1
questo non può essere usato per esempio in Linq to SQL
Romain Vergnory,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.