Mutando el árbol de expresión de un predicado para apuntar a otro tipo

c# expression-trees lambda linq

Pregunta

Introducción

En la aplicación en la que estoy trabajando actualmente, hay dos tipos de cada objeto comercial: el tipo "ActiveRecord" y el tipo "DataContract". Así, por ejemplo, habría:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

La capa de acceso a la base de datos se encarga de traducir entre familias: puede indicarle que actualice un DataContract.Widget y mágicamente creará un ActiveRecord.Widget con los mismos valores de propiedad y lo guardará.

El problema surgió al intentar refactorizar esta capa de acceso a la base de datos.

El problema

Quiero agregar métodos como los siguientes a la capa de acceso a la base de datos:

// Widget is DataContract.Widget

interface IDbAccessLayer {
    IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}

Lo anterior es un método simple de "uso general" con predicado personalizado. El único punto de interés es que estoy pasando un árbol de expresiones en lugar de un lambda porque dentro de IDbAccessLayer estoy consultando un IQueryable<ActiveRecord.Widget> ; para hacerlo de manera eficiente (piense en LINQ to SQL) necesito pasar un árbol de expresiones, por lo que este método solo solicita eso.

El problema: el parámetro debe transformarse mágicamente de una Expression<Func<DataContract.Widget, bool>> a una Expression<Func<ActiveRecord.Widget, bool>> .

Intento de solucion

Lo que me gustaría hacer dentro de GetMany es:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        predicate.Body,
        predicate.Parameters);

    // use lambda to query ActiveRecord.Widget and return some value
}

Esto no funcionará porque en un escenario típico, por ejemplo si:

predicate == w => w.Id == 0;

... el árbol de expresiones contiene una instancia de MemberAccessExpression que tiene una propiedad de tipo MemberInfo que describe DataContract.Widget.Id . También hay instancias de ParameterExpression tanto en el árbol de expresiones como en su colección de parámetros ( predicate.Parameters ) que describen DataContract.Widget ; Todo esto generará errores, ya que el cuerpo consultable no contiene ese tipo de widget, sino ActiveRecord.Widget .

Después de buscar un poco, encontré System.Linq.Expressions.ExpressionVisitor (su fuente se puede encontrar aquí en el contexto de un manual), que ofrece una forma conveniente de modificar un árbol de expresiones. En .NET 4, esta clase se incluye fuera de la caja.

Armado con esto, implementé un visitante. Este simple visitante solo se encarga de cambiar los tipos en el acceso de miembros y las expresiones de parámetros, pero esa es la funcionalidad suficiente para trabajar con el predicado w => w.Id == 0 .

internal class Visitor : ExpressionVisitor
{
    private readonly Func<Type, Type> typeConverter;

    public Visitor(Func<Type, Type> typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var dataContractType = node.Member.ReflectedType;
        var activeRecordType = this.typeConverter(dataContractType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            activeRecordType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var dataContractType = node.Type;
        var activeRecordType = this.typeConverter(dataContractType);

        return Expression.Parameter(activeRecordType, node.Name);
    }
}

Con este visitante, GetMany convierte en:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var visitor = new Visitor(...);
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        visitor.Visit(predicate.Body),
        predicate.Parameters.Select(p => visitor.Visit(p));

    var widgets = ActiveRecord.Widget.Repository().Where(lambda);

    // This is just for reference, see below
    Expression<Func<ActiveRecord.Widget, bool>> referenceLambda = 
        w => w.Id == 0;

    // Here we 'd convert the widgets to instances of DataContract.Widget and
    // return them -- this has nothing to do with the question though.
}

Resultados

La buena noticia es que lambda se construye bien. La mala noticia es que no está funcionando; me explota cuando trato de usarlo, y los mensajes de excepción realmente no son útiles en absoluto.

He examinado la lambda que produce mi código y una lambda codificada con la misma expresión; se ven exactamente iguales Pasé horas en el depurador tratando de encontrar alguna diferencia, pero no puedo.

Cuando el predicado es w => w.Id == 0 , lambda ve exactamente igual que referenceLambda . Pero el último funciona con, por ejemplo, IQueryable<T>.Where , mientras que el primero no lo hace; He intentado esto en la ventana inmediata del depurador.

También debo mencionar que cuando el predicado es w => true , todo funciona bien. Por lo tanto, asumo que no estoy haciendo suficiente trabajo en el visitante, pero no puedo encontrar más pistas a seguir.

Solución final

Después de tener en cuenta las respuestas correctas al problema (dos de ellas a continuación; una breve, una con código), el problema se resolvió; Pongo el código junto con algunas notas importantes en una respuesta por separado para evitar que esta larga pregunta se vuelva aún más larga.

Gracias a todos por vuestras respuestas y comentarios!

Respuesta aceptada

Parece que estás generando la expresión del parámetro dos veces, en VisitMember () aquí:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

... desde base.Visit () terminará en VisitParameter, imagino, y en GetMany () en sí:

var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
    visitor.Visit(predicate.Body),
    predicate.Parameters.Select(p => visitor.Visit(p));

Si está utilizando una ParameterExpression en el cuerpo, tiene que ser la misma instancia (no solo el mismo tipo y nombre) que la declarada para Lambda. He tenido problemas antes con este tipo de escenario, aunque creo que el resultado fue que simplemente no pude crear la expresión, solo sería una excepción. En cualquier caso, puede intentar reutilizar la instancia del parámetro para ver si ayuda.


Respuesta popular

Resultó que la parte delicada es simplemente que las instancias de ParameterExpression que existen en el árbol de expresiones de la nueva lambda deben ser las mismas instancias que se pasan en el IEnumerable<ParameterExpression> de Expression.Lambda .

Tenga en cuenta que dentro de TransformPredicateLambda , doy t => typeof(TNewTarget) como la función "convertidor de tipo"; eso es porque en este caso específico, podemos asumir que todos los parámetros y accesos de miembros serán de ese tipo específico. Escenarios más avanzados pueden necesitar lógica adicional allí.

El código:

internal class DbAccessLayer {
    private static Expression<Func<TNewTarget, bool>> 
    TransformPredicateLambda<TOldTarget, TNewTarget>(
    Expression<Func<TOldTarget, bool>> predicate)
    {
        var lambda = (LambdaExpression) predicate;
        if (lambda == null) {
            throw new NotSupportedException();
        }

        var mutator = new ExpressionTargetTypeMutator(t => typeof(TNewTarget));
        var explorer = new ExpressionTreeExplorer();
        var converted = mutator.Visit(predicate.Body);

        return Expression.Lambda<Func<TNewTarget, bool>>(
            converted,
            lambda.Name,
            lambda.TailCall,
            explorer.Explore(converted).OfType<ParameterExpression>());
    }


    private class ExpressionTargetTypeMutator : ExpressionVisitor
    {
        private readonly Func<Type, Type> typeConverter;

        public ExpressionTargetTypeMutator(Func<Type, Type> typeConverter)
        {
            this.typeConverter = typeConverter;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            var dataContractType = node.Member.ReflectedType;
            var activeRecordType = this.typeConverter(dataContractType);

            var converted = Expression.MakeMemberAccess(
                base.Visit(node.Expression), 
                activeRecordType.GetProperty(node.Member.Name));

            return converted;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var dataContractType = node.Type;
            var activeRecordType = this.typeConverter(dataContractType);

            return Expression.Parameter(activeRecordType, node.Name);
        }
    }
}

/// <summary>
/// Utility class for the traversal of expression trees.
/// </summary>
public class ExpressionTreeExplorer
{
    private readonly Visitor visitor = new Visitor();

    /// <summary>
    /// Returns the enumerable collection of expressions that comprise
    /// the expression tree rooted at the specified node.
    /// </summary>
    /// <param name="node">The node.</param>
    /// <returns>
    /// The enumerable collection of expressions that comprise the expression tree.
    /// </returns>
    public IEnumerable<Expression> Explore(Expression node)
    {
        return this.visitor.Explore(node);
    }

    private class Visitor : ExpressionVisitor
    {
        private readonly List<Expression> expressions = new List<Expression>();

        protected override Expression VisitBinary(BinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBinary(node);
        }

        protected override Expression VisitBlock(BlockExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBlock(node);
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConditional(node);
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConstant(node);
        }

        protected override Expression VisitDebugInfo(DebugInfoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDebugInfo(node);
        }

        protected override Expression VisitDefault(DefaultExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDefault(node);
        }

        protected override Expression VisitDynamic(DynamicExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDynamic(node);
        }

        protected override Expression VisitExtension(Expression node)
        {
            this.expressions.Add(node);
            return base.VisitExtension(node);
        }

        protected override Expression VisitGoto(GotoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitGoto(node);
        }

        protected override Expression VisitIndex(IndexExpression node)
        {
            this.expressions.Add(node);
            return base.VisitIndex(node);
        }

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            this.expressions.Add(node);
            return base.VisitInvocation(node);
        }

        protected override Expression VisitLabel(LabelExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLabel(node);
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            this.expressions.Add(node);
            return base.VisitLambda(node);
        }

        protected override Expression VisitListInit(ListInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitListInit(node);
        }

        protected override Expression VisitLoop(LoopExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLoop(node);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMember(node);
        }

        protected override Expression VisitMemberInit(MemberInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMemberInit(node);
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMethodCall(node);
        }

        protected override Expression VisitNew(NewExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNew(node);
        }

        protected override Expression VisitNewArray(NewArrayExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNewArray(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            this.expressions.Add(node);
            return base.VisitParameter(node);
        }

        protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
        {
            this.expressions.Add(node);
            return base.VisitRuntimeVariables(node);
        }

        protected override Expression VisitSwitch(SwitchExpression node)
        {
            this.expressions.Add(node);
            return base.VisitSwitch(node);
        }

        protected override Expression VisitTry(TryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTry(node);
        }

        protected override Expression VisitTypeBinary(TypeBinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTypeBinary(node);
        }

        protected override Expression VisitUnary(UnaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitUnary(node);
        }

        public IEnumerable<Expression> Explore(Expression node)
        {
            this.expressions.Clear();
            this.Visit(node);
            return expressions.ToArray();
        }
    }
}


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