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

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". Entonces, 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 unDataContract.Widget y mágicamente creará unActiveRecord.Widget con los mismos valores de propiedad y guárdelo en su lugar.

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

El problema

Quiero agregar métodos como el siguiente 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 "get" de uso general con predicado personalizado. El único punto de interés es que estoy pasando un árbol de expresión en lugar de un lambda porque adentroIDbAccessLayer Estoy consultando unIQueryable<ActiveRecord.Widget>; para hacerlo de manera eficiente (piense en LINQ to SQL) necesito pasar un árbol de expresión para que este método pida eso.

El inconveniente: el parámetro debe transformarse mágicamente de unExpression<Func<DataContract.Widget, bool>> a unaExpression<Func<ActiveRecord.Widget, bool>>.

Intento de solución

Lo que me gustaría hacer adentroGetMany 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 expresión contiene unMemberAccessExpression instancia que tiene una propiedad de tipoMemberInfo eso describeDataContract.Widget.Id. También hayParameterExpression instancias tanto en el árbol de expresión como en su colección de parámetros (predicate.Parameters) que describenDataContract.Widget; todo esto generará errores, ya que el cuerpo consultable no contiene ese tipo de widget, sino más bienActiveRecord.Widget.

Después de buscar un poco, encontréSystem.Linq.Expressions.ExpressionVisitor (su fuente se puede encontraraquí en el contexto de un tutorial), que ofrece una forma conveniente de modificar un árbol de expresión. 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 eso es suficiente funcionalidad para trabajar con el predicadow => 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 visitanteGetMany se 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 quelambda Está construido muy bien. La mala noticia es que no está funcionando; me está explotando 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 esw => w.Id == 0, lambda se ve exactamente comoreferenceLambda. Pero este último funciona con p.IQueryable<T>.Where, mientras que el primero no; He intentado esto en la ventana inmediata del depurador.

También debo mencionar que cuando el predicado esw => trueTodo funciona bien. Por lo tanto, supongo que no estoy haciendo suficiente trabajo en el visitante, pero no puedo encontrar más pistas para 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ó; Puse el código junto con algunas notas importantes en unrespuesta separada para evitar que esta larga pregunta se vuelva aún más larga.

¡Gracias a todos por sus respuestas y comentarios!

Respuestas a la pregunta(6)

Su respuesta a la pregunta