Estoy tratando de construir un árbol de expresión mediante programación.

Tengo en mi entrada, una lista de clases de condición que tienen la siguiente forma:

public class Filter
{
    public string field { get; set; }
    public string operator { get; set; }
    public string value { get; set; }
}

Cuando construyo el objeto Expression, creo un Expression para cada condición de la siguiente manera

foreach ( Filter sf in rules ) {
    Expression ex = sf.ToExpression( query );
    if ( mainExpression == null ) {
        mainExpression = ex;
    }
    else {
        if ( logicalCondition == "AND" ) {
            mainExpression = Expression.And( mainExpression, ex );
        }
        else if ( logicalCondition == "OR" ) {
            mainExpression = Expression.Or( mainExpression, ex );
        }
    }
}

El método Filter.ToExpression () se implementa así

public override Expression ToExpression( IQueryable query ) {
    ParameterExpression parameter = Expression.Parameter( query.ElementType, "p" );
    MemberExpression memberAccess = null;
    foreach ( var property in field.Split( '.' ) )
        memberAccess = MemberExpression.Property( memberAccess ?? ( parameter as Expression ), property );
    ConstantExpression filter = Expression.Constant( Convert.ChangeType( value, memberAccess.Type ) );
    WhereOperation condition = (WhereOperation)StringEnum.Parse( typeof( WhereOperation ), operator );
    LambdaExpression lambda = BuildLambdaExpression( memberAccess, filter, parameter, condition, value );
    return lambda;
}

Todo funciona cuando tengo una sola condición, pero cuando trato de combinar expresiones usando uno de los métodos estáticos And, Or, AndAlso, OrElse, recibo un InvalidOperationException que dice:

El operador binario Or no está definido para los tipos 'System.Func 2 [MyObject, System.Boolean]' y 'System.Func 2 [MyObject, System.Boolean]'.

Me estoy confundiendo un poco. ¿Alguien puede explicar mejor las razones de la excepción y sugerir una solución?

¡Muchas gracias!

15
Lorenzo 10 feb. 2012 a las 20:54

1 respuesta

La mejor respuesta

Estás combinando a => a == 3 y a => a == 4 en (a => a == 3) || (a => a == 4), pero en su lugar deberías intentar hacerlo a => (a == 3 || a == 4). Esto no es demasiado difícil de hacer manualmente, pero alguien ya lo ha hecho por ti. Busque "Combinación de expresiones".

Editar : según lo solicitado, un ejemplo sencillo de cómo hacer esto manualmente.

Edición 2 : usa ExpressionVisitor que es nuevo en .NET 4, pero en MSDN puede encontrar una implementación utilizable para versiones anteriores. Supongo que el código MSDN no califica como "tercero" para sus propósitos. Solo necesita cambiar el método protected virtual Expression Visit(Expression exp) a public. Y como Enumerable.Zip no está disponible para usted y no es necesario, ya no está.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

namespace DemoApp
{
    <include ExpressionVisitor definition here for .NET 3.5>

    public class ExpressionParameterReplacer : ExpressionVisitor
    {
        public ExpressionParameterReplacer(IList<ParameterExpression> fromParameters, IList<ParameterExpression> toParameters)
        {
            ParameterReplacements = new Dictionary<ParameterExpression, ParameterExpression>();
            for (int i = 0; i != fromParameters.Count && i != toParameters.Count; i++)
                ParameterReplacements.Add(fromParameters[i], toParameters[i]);
        }
        private IDictionary<ParameterExpression, ParameterExpression> ParameterReplacements
        {
            get;
            set;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            ParameterExpression replacement;
            if (ParameterReplacements.TryGetValue(node, out replacement))
                node = replacement;
            return base.VisitParameter(node);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Expression<Func<int, bool>> exprA = a => a == 3;
            Expression<Func<int, bool>> exprB = b => b == 4;
            Expression<Func<int, bool>> exprC =
                Expression.Lambda<Func<int, bool>>(
                    Expression.OrElse(
                        exprA.Body,
                        new ExpressionParameterReplacer(exprB.Parameters, exprA.Parameters).Visit(exprB.Body)),
                    exprA.Parameters);
            Console.WriteLine(exprA.ToString());
            Console.WriteLine(exprB.ToString());
            Console.WriteLine(exprC.ToString());
            Func<int, bool> funcA = exprA.Compile();
            Func<int, bool> funcB = exprB.Compile();
            Func<int, bool> funcC = exprC.Compile();
            Debug.Assert(funcA(3) && !funcA(4) && !funcA(5));
            Debug.Assert(!funcB(3) && funcB(4) && !funcB(5));
            Debug.Assert(funcC(3) && funcC(4) && !funcC(5));
        }
    }
}
34
11 feb. 2012 a las 18:20
Hola, gracias por su respuesta. No puedo usar código de terceros para resolver este problema. ¿Podría explicar mejor cuál sería la forma de hacerlo manualmente? ¡Gracias de nuevo!
 – 
Lorenzo
10 feb. 2012 a las 21:55
Claro, agregué un programa basado en las dos expresiones que usé como ejemplo.
 – 
user743382
10 feb. 2012 a las 22:10
Hola, he intentado implementar tu solución. Comprendí que ExpressionVisitor proviene de las fuentes de LinqKit y he podido ver cómo funciona. La pregunta ahora es: ¿de dónde viene el método Zip de IEnumerable<ParameterExpression>? Estoy usando .NET 3.5 y no puedo encontrar ese método :(
 – 
Lorenzo
11 feb. 2012 a las 17:57
Ah, estaba usando .NET 4, que viene con la clase ExpressionVisitor y el método de extensión Enumerable.Zip. Veré si puedo hacer que algo funcione para 3.5 también, pero eso será un poco más complicado.
 – 
user743382
11 feb. 2012 a las 18:01
Resulta que MS ha publicado un ExpressionVisitor funcional en MSDN, así que probé para ver si funciona (funciona) e hice una mención al respecto. Y Enumerable.Zip no es realmente necesario, así que lo dejé caer y agregué un bucle for simple en su lugar.
 – 
user743382
11 feb. 2012 a las 18:22