¿Cómo componer Expresión: selector + predicado?

entity-framework expression-trees lambda linq

Pregunta

Supongamos que tenemos dos clases

public class EntityA
{
    public EntityB EntityB { get; set; }
}

public class EntityB
{
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
}

Y dos expresiones para selector y predicador.

Expression<Func<EntityA, EntityB>> selector = c => c.EntityB;
Expression<Func<EntityB, bool>> predicate = c => c.IsDeleted && c.Name == "AAA";

Necesito escribir un método que devuelva una expresión compuesta como una

Expression<Func<TSource, bool>> Compose<TPropType>(Expression<Func<TSource, TPropType>> selector, Expression<Func<TPropType, bool>> predicator)
{
    // Expression API ???
}

En mi ejemplo el resultado debería ser

Expression<Func<EntityA, bool>> exp = Compose(selector, predicate);

que es equivalente a

Expression<Func<EntityA, bool>> exp = c => c.EntityB.IsDeleted && c.EntityB.Name == "AAA";

Gracias por adelantado.

Respuesta aceptada

Puedes probar lo siguiente:

static Expression<Func<TSource, bool>> Compose<TSource, TPropType>(
    Expression<Func<TSource, TPropType>> selector,
    Expression<Func<TPropType, bool>> predicator)
{
    ParameterExpression param = Expression.Parameter(typeof(TSource), "sourceObj");
    Expression invokedSelector = Expression.Invoke(selector, new Expression[] { param });
    Expression invokedPredicate = Expression.Invoke(predicator, new[] { invokedSelector });

    return Expression.Lambda<Func<TSource, bool>>(invokedPredicate, new[] { param });
}

Aquí está cómo usarlo:

static void Main()
{
    Expression<Func<EntityA, EntityB>> selector = c => c.EntityB;
    Expression<Func<EntityB, bool>> predicate = c => c.IsDeleted && c.Name == "AAA";

    Expression<Func<EntityA, bool>> exp = Compose(selector, predicate);
    System.Console.WriteLine(exp.Compile()(new EntityA()));
}

Respuesta popular

Invocar estas expresiones lambda es ciertamente algo que no quieres hacer. Lo que deberías estar haciendo es reescribir las expresiones. Solo necesitarás una forma de vincular los valores a las expresiones lambda como si los hubieras invocado. Para hacerlo, reescriba los cuerpos de las expresiones reemplazando los parámetros con los valores a los que está vinculado. Puede usar este SubstitutionVisitor de SubstitutionVisitor para ayudar a hacer eso:

public class SubstitutionVisitor : ExpressionVisitor
{
    public Expression OldExpr { get; set; }
    public Expression NewExpr { get; set; }

    public override Expression Visit(Expression node)
    {
        return (node == OldExpr) ? NewExpr : base.Visit(node);
    }
}

Dadas estas expresiones por ejemplo:

Expression<Func<EntityA, EntityB>> selector =
    entityA => entityA.EntityB;
Expression<Func<EntityB, bool>> predicate =
    entityB => entityB.IsDeleted && entityB.Name == "AAA";

El objetivo es reescribirlo de manera efectiva para que se vuelva así:

Expression<Func<EntityA, bool>> composed =
    entity => entity.EntityB.IsDeleted && entity.EntityB.Name == "AAA";
static Expression<Func<TSource, bool>> Compose<TSource, TProp>(
    Expression<Func<TSource, TProp>> selector,
    Expression<Func<TProp, bool>> predicate)
{
    var parameter = Expression.Parameter(typeof(TSource), "entity");
    var property = new SubstitutionVisitor
    {
        OldExpr = selector.Parameters.Single(),
        NewExpr = parameter,
    }.Visit(selector.Body);
    var body = new SubstitutionVisitor
    {
        OldExpr = predicate.Parameters.Single(),
        NewExpr = property,
    }.Visit(predicate.Body);
    return Expression.Lambda<Func<TSource, bool>>(body, parameter);
}

Para entender lo que está pasando aquí, aquí hay una explicación línea por línea:

  1. Crea un nuevo parámetro para el nuevo lambda que estamos creando.

    entity => ...
    
  2. Dado el selector, reemplace todas las instancias del parámetro original entityA con nuestra nueva entity parámetro del cuerpo de la lambda para obtener la propiedad.

    entityA => entityA.EntityB
    // becomes
    entity.EntityB
    
  3. Dado el predicado, reemplace todas las instancias del parámetro original entityB con la propiedad entity.EntityB obtenida entity.EntityB del cuerpo de la lambda para obtener el cuerpo de nuestra nueva lambda.

    entityB => entityB.IsDeleted && entityB.Name == "AAA"
    // becomes
    entity.EntityB.IsDeleted && entity.EntityB.Name == "AAA"
    
  4. Ponlo todo junto en la nueva lambda.

    entity => entity.EntityB.IsDeleted && entity.EntityB.Name == "AAA"
    


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow