Mutare l'albero delle espressioni di un predicato per scegliere come target un altro tipo

c# expression-trees lambda linq

Domanda

Intro

Nell'applicazione a cui sto lavorando attualmente, ci sono due tipi di ogni oggetto di business: il tipo "ActiveRecord" e il tipo "DataContract". Quindi, per esempio, ci sarebbe:

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

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

Il livello di accesso al database si occupa della traduzione tra famiglie: puoi dire di aggiornare un DataContract.Widget e creerà magicamente un ActiveRecord.Widget con gli stessi valori di proprietà e lo salverà.

Il problema è emerso durante il tentativo di refactoring questo livello di accesso al database.

Il problema

Voglio aggiungere metodi come il seguente al livello di accesso al database:

// Widget is DataContract.Widget

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

Quanto sopra è un semplice metodo "get" di uso generale con predicato personalizzato. L'unico punto di interesse è che sto passando un albero di espressioni invece di un lambda perché all'interno di IDbAccessLayer sto interrogando un IQueryable<ActiveRecord.Widget> ; per farlo in modo efficiente (penso LINQ a SQL) ho bisogno di passare in un albero di espressione quindi questo metodo richiede proprio questo.

L'intoppo: il parametro deve essere trasformato magicamente da Expression<Func<DataContract.Widget, bool>> a Expression<Func<ActiveRecord.Widget, bool>> .

Tentativo di soluzione

Quello che mi piacerebbe fare all'interno di GetMany è:

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
}

Questo non funzionerà perché in uno scenario tipico, ad esempio se:

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

... l'albero delle espressioni contiene un'istanza MemberAccessExpression che ha una proprietà di tipo MemberInfo che descrive DataContract.Widget.Id . Esistono anche istanze ParameterExpression sia nella struttura dell'espressione che nella sua collezione di parametri ( predicate.Parameters ) che descrivono DataContract.Widget ; tutto ciò si tradurrà in errori poiché il corpo interrogabile non contiene quel tipo di widget ma piuttosto ActiveRecord.Widget .

Dopo aver cercato un po ', ho trovato System.Linq.Expressions.ExpressionVisitor (la sua origine può essere trovata qui nel contesto di un how-to), che offre un modo conveniente per modificare un albero di espressioni. In .NET 4, questa classe è inclusa nella confezione.

Armato di questo, ho implementato un visitatore. Questo semplice visitatore si occupa solo della modifica dei tipi di accesso dei membri e delle espressioni dei parametri, ma è sufficiente funzionalità per lavorare con il predicato 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 questo visitatore, GetMany diventa:

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.
}

risultati

La buona notizia è che lambda è costruita bene. La cattiva notizia è che non funziona; mi sta facendo esplodere quando cerco di usarlo, e i messaggi di eccezione non sono affatto utili.

Ho esaminato il lambda che il mio codice produce e un lambda hardcoded con la stessa espressione; sembrano esattamente uguali. Ho passato ore nel debugger cercando di trovare qualche differenza, ma non posso.

Quando il predicato è w => w.Id == 0 , lambda appare esattamente come referenceLambda . Ma quest'ultimo funziona con IQueryable<T>.Where , mentre il primo no; Ho provato questo nella finestra immediata del debugger.

Dovrei anche menzionare che quando il predicato è w => true , tutto funziona correttamente. Quindi presumo che non stia facendo abbastanza lavoro nel visitatore, ma non riesco a trovare altri indizi da seguire.

Soluzione finale

Dopo aver preso in considerazione le risposte corrette al problema (due di seguito: uno corto, uno con codice) il problema è stato risolto; Ho inserito il codice insieme ad alcune note importanti in una risposta separata per evitare che questa lunga domanda diventi ancora più lunga.

Grazie a tutti per le vostre risposte e commenti!

Risposta accettata

Sembra che tu stia generando l'espressione del parametro due volte, in VisitMember () qui:

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

... poiché base.Visit () finirà in VisitParameter che immagino e in GetMany () stesso:

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

Se stai usando ParameterExpression nel corpo, deve essere la stessa istanza (non solo lo stesso tipo e nome) di quella dichiarata per il Lambda. Ho avuto problemi in passato con questo tipo di scenario, anche se penso che il risultato è stato che non ero in grado di creare l'espressione, sarebbe solo un'eccezione. In ogni caso potresti provare a riutilizzare l'istanza del parametro per vedere se è d'aiuto.


Risposta popolare

Si è scoperto che la parte difficile è semplicemente che le istanze ParameterExpression presenti nell'albero delle espressioni del nuovo lambda devono essere le stesse istanze passate nel IEnumerable<ParameterExpression> di Expression.Lambda .

Si noti che all'interno di TransformPredicateLambda sto dando t => typeof(TNewTarget) come funzione "type converter"; questo perché in questo caso specifico, possiamo assumere che tutti i parametri e gli accessi ai membri saranno di quel tipo specifico. Gli scenari più avanzati potrebbero richiedere una logica aggiuntiva.

Il codice:

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


Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché