Mutieren des Ausdrucksbaums eines Prädikats, um auf einen anderen Typ abzuzielen

c# expression-trees lambda linq

Frage

Einführung

In der Anwendung, an der ich gerade arbeite, gibt es für jedes Geschäftsobjekt zwei Arten: die Art "ActiveRecord" und die Art "DataContract". Zum Beispiel würde es sein:

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

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

Die Datenbankzugriffsebene sorgt für die Übersetzung zwischen den Familien: Sie können festlegen, dass ein DataContract.Widget aktualisiert wird, und es wird auf magische Weise ein ActiveRecord.Widget mit den gleichen Eigenschaftswerten erstellt und stattdessen gespeichert.

Das Problem trat auf, als versucht wurde, diese Datenbankzugriffsebene neu zu strukturieren.

Das Problem

Ich möchte der Datenbankzugriffsebene folgende Methoden hinzufügen:

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

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

Das obige ist eine einfache "get" -Methode mit benutzerdefiniertem Prädikat. Der einzige interessante Punkt ist, dass ich einen Ausdrucksbaum anstelle eines Lambda übergebe, weil ich in IDbAccessLayer ein IQueryable<ActiveRecord.Widget> ; Um dies effizient zu tun (denke an LINQ to SQL) muss ich einen Ausdrucksbaum übergeben, so dass diese Methode genau danach fragt.

Der Haken: Der Parameter muss von einem Expression<Func<DataContract.Widget, bool>> in einen Expression<Func<ActiveRecord.Widget, bool>> .

Versuchte Lösung

Was ich in GetMany machen GetMany ist:

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

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

Dies funktioniert nicht, weil in einem typischen Szenario zum Beispiel:

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

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

... der Ausdrucksbaum enthält eine MemberAccessExpression Instanz , die eine Eigenschaft des Typs hat MemberInfo die beschreibt DataContract.Widget.Id . Es gibt auch ParameterExpression Instanzen sowohl in der Ausdrucksbaumstruktur als auch in ihrer Parametersammlung ( predicate.Parameters ), die DataContract.Widget beschreiben. All dies führt zu Fehlern, da der abfragbare Textkörper nicht diesen Typ von Widget, sondern ActiveRecord.Widget .

Nach ein wenig suchen, fand ich System.Linq.Expressions.ExpressionVisitor (seine Quelle kann hier im Rahmen einer System.Linq.Expressions.ExpressionVisitor gefunden werden ), die eine bequeme Möglichkeit bietet, einen Ausdrucksbaum zu modifizieren. In .NET 4 ist diese Klasse standardmäßig enthalten.

Mit diesem Ziel habe ich einen Besucher implementiert. Dieser einfache Besucher kümmert sich nur darum, die Typen im Member-Zugriff und in den Parameterausdrücken zu ändern, aber das ist genug Funktionalität, um mit dem Prädikat w => w.Id == 0 .

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

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

Mit diesem Besucher wird GetMany :

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

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

Ergebnisse

Die gute Nachricht ist, dass lambda konstruiert ist. Die schlechte Nachricht ist, dass es nicht funktioniert; Es explodiert bei mir, wenn ich versuche, es zu benutzen, und die Ausnahmemeldungen sind wirklich überhaupt nicht hilfreich.

Ich habe das Lambda, das mein Code erzeugt, und ein hartkodiertes Lambda mit dem gleichen Ausdruck untersucht; sie sehen genau gleich aus. Ich verbrachte Stunden im Debugger, um einen Unterschied zu finden, aber ich kann nicht.

Wenn das Prädikat w => w.Id == 0 , sieht lambda genau wie referenceLambda . Aber letzteres funktioniert zB mit IQueryable<T>.Where , während ersteres nicht funktioniert; Ich habe dies im unmittelbaren Fenster des Debuggers versucht.

Ich sollte auch erwähnen, dass, wenn das Prädikat w => true , alles gut funktioniert. Deshalb gehe ich davon aus, dass ich nicht genug Arbeit im Besucher mache, aber ich kann keine weiteren Hinweise finden, denen ich folgen kann.

Endgültige Lösung

Nach Berücksichtigung der richtigen Antworten auf das Problem (zwei von ihnen unten, eine kurze, eine mit Code) wurde das Problem gelöst; Ich habe den Code zusammen mit einigen wichtigen Notizen in einer separaten Antwort eingefügt, damit diese lange Frage nicht noch länger wird.

Danke an alle für eure Antworten und Kommentare!

Akzeptierte Antwort

Es scheint, dass Sie den Parameter-Ausdruck in VisitMember () hier zweimal generieren:

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

... da base.Visit () in VisitParameter enden wird, stelle ich mir vor und in GetMany () selbst:

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

Wenn Sie einen ParameterExpression im body verwenden, muss es dieselbe Instanz sein (nicht nur derselbe Typ und Name) wie für Lambda deklariert. Ich hatte vorher Probleme mit dieser Art von Szenario, obwohl ich denke, das Ergebnis war, dass ich einfach nicht in der Lage war, den Ausdruck zu erstellen, es würde nur eine Ausnahme auslösen. In jedem Fall könnten Sie versuchen, die Parameterinstanz erneut zu verwenden, um zu sehen, ob sie hilft.


Beliebte Antwort

Es stellte sich heraus, dass der schwierige Teil einfach darin besteht, dass die ParameterExpression Instanzen, die in der Ausdrucksbaumstruktur des neuen Lambda vorhanden sind, dieselben Instanzen sein müssen, die im IEnumerable<ParameterExpression> von Expression.Lambda .

Beachten Sie, dass ich innerhalb von TransformPredicateLambda t => typeof(TNewTarget) als "type converter" -Funktion t => typeof(TNewTarget) ; Das liegt daran, dass wir in diesem speziellen Fall davon ausgehen können, dass alle Parameter und Member-Zugriffe von genau diesem Typ sind. Fortgeschrittenere Szenarien benötigen möglicherweise zusätzliche Logik.

Der Code:

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



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum