Combinando dos expresiones (Expression <Func <T, bool >>)

249

Tengo dos expresiones de tipo Expression<Func<T, bool>>y quiero tomar OR, AND o NOT de estas y obtener una nueva expresión del mismo 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
BjartN
fuente
8
Mensaje muy útil que recibí de Google: LINQ to Entities: Combining Predicates
Thomas CG de Vilhena

Respuestas:

331

Bueno, puedes usar Expression.AndAlso/ OrElseetc para combinar expresiones lógicas, pero el problema son los parámetros; ¿Estás trabajando con lo mismo ParameterExpressionen expr1 y expr2? Si es así, es más fácil:

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

Esto también funciona bien para negar una sola operación:

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

De lo contrario, dependiendo del proveedor de LINQ, puede combinarlos 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;
}

En algún lugar, tengo un código que reescribe un nodo de reemplazo de árbol de expresión para eliminar la necesidad Invoke, pero es bastante largo (y no puedo recordar dónde lo dejé ...)


Versión generalizada que elige la ruta más 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);
}

A partir de .NET 4.0, existe la ExpressionVisitorclase que le permite crear expresiones que son seguras para 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
fuente
Hola Marc, probé tu primera sugerencia, en el primer bloque de código anterior, pero cuando paso la expresión "lambda" <func <T, bool >> resulta en un método Where, obtengo un error diciendo que el parámetro es ¿fuera del ámbito? ¿alguna idea? aplausos
andy
1
+1 la versión generalizada funciona a las mil maravillas, usé Y en lugar de andalso, pensé que linq to sql no es compatible con andalso.
Maslow
2
@Maslow: aquí hay una reescritura que puede alinear los árboles para salvar Invoke: stackoverflow.com/questions/1717444/…
Marc Gravell
1
@Aron ahora mira la fecha: el visitante de .NET framework ( ExpressionVisitor) no existía en ese entonces; Tengo un ejemplo relacionado sobre stackoverflow de una fecha similar donde implementa el visitante manualmente: es mucho código.
Marc Gravell
1
@ MarkGravell, estoy usando su primera solución para combinar mis expresiones, y todo funciona bien incluso en el marco de la entidad, ¿cuáles serían los beneficios de usar la última solución?
Johnny 5
62

Puede usar Expression.AndAlso / OrElse para combinar expresiones lógicas, pero debe asegurarse de que las Expresiones de parámetros sean las mismas.

Estaba teniendo problemas con EF y PredicateBuilder, así que hice el mío sin recurrir a Invoke, que podría usar así:

var filterC = filterA.And(filterb);

Código fuente para mi 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);
    }   
}

Y la clase de utilidad para sustituir los parámetros en una 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
fuente
Esta solución fue la única que me permitió tener x => x.Property == Value combinado con arg => arg.Property2 == Value. Accesorios importantes, un poco concisos y confusos, pero funciona, así que no me voy a quejar. Kudos Adam :-)
VulgarBinary
Esta es una gran solución.
Aaron Stainback
Adam, esto resolvió un problema muy molesto que tenía al usar el proveedor Linq del modelo de objetos de cliente de SharePoint, gracias por publicarlo.
Christopher McAtackney
¡Esto funcionó para mí! Había buscado una variedad de soluciones, así como un generador de predicados y nada funcionó hasta esto. ¡Gracias!
tokyo0709
Este es un maravilloso fragmento de código. No pude encontrar un lugar para ajustar el código, copiar y pegar y eso es todo :)
Tolga Evcimen
19

Si su proveedor no es compatible con Invoke y necesita combinar dos expresiones, puede usar un ExpressionVisitor para reemplazar el parámetro en la segunda expresión por el parámetro en la primera expresión.

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" }));
}
Francisco
fuente
1
Esto resolvió mi problema particular donde la otra solución resultó en la misma excepción. Gracias.
Shaun Wilson
1
Esta es una gran solución.
Aaron Stainback
3

No hay nada nuevo aquí, pero casé esta respuesta con esta respuesta y la refacté un poco para que incluso yo entienda lo que está sucediendo:

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
fuente
Estaba teniendo dificultades para comprender el concepto, y su combinación de otras respuestas me ayudó a hacer clic para mí. ¡Gracias!
Kevin M. Lapio
2

Necesitaba lograr los mismos resultados, pero usando algo más genérico (ya que el tipo no se conocía). Gracias a la respuesta de Marc, finalmente descubrí lo que estaba tratando de lograr:

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

Sugiero una mejora más para PredicateBuilder y sus ExpressionVisitorsoluciones. Lo llamé UnifyParametersByNamey puedes encontrarlo en la biblioteca mita de MIT: LinqExprHelper . Permite combinar expresiones arbitrarias lambda. Por lo general, las preguntas se hacen sobre la expresión predicada, pero esta idea también se extiende a las expresiones de proyección.

El siguiente código emplea un método ExprAdresque crea una expresión parametrizada complicada, usando lambda en línea. Esta expresión complicada se codifica solo una vez y luego se reutiliza, gracias a la LinqExprHelpermini biblioteca.

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

Y este es el código de construcción de subexpresión:

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

Lo que intenté lograr fue realizar consultas parametrizadas sin necesidad de copiar y pegar y con la capacidad de usar lambdas en línea, que son muy bonitas. Sin todas estas cosas de expresión auxiliar, me vería obligado a crear una consulta completa de una vez.

Jarekczek
fuente
-7

Creo que esto funciona bien, ¿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));
Céline
fuente
1
esto no se puede usar en Linq to SQL, por ejemplo
Romain Vergnory