ExpressionTree method to Assign MemberInitExpression to property

假装没事ソ 提交于 2019-12-24 18:22:11

问题


I am trying to use expression trees so that I can choose to map across to a DTO using entity framework in much the same way as the Include directive works on a DbSet (part of an open sorce project implementing OData).

The code below represents a test case.

Expression<Func<Bar, Bar>> mapBar = b => new Bar { BarInt = b.BarInt, BarString = b.BarString };
Expression<Func<Foo, Foo>> mapFoo = f => new Foo { FooInt = f.FooInt, B = null };

Expression<Func<Foo, Foo>> target = f => new Foo { FooInt = f.FooInt, 
    B = new Bar { 
        BarInt=f.B.BarInt, BarString = f.B.BarString 
    } };

note Foo.B was null, and has the mapBar expression inserted.

I have got as far as adding the navigation property using the following ExpressionVisitor

public class UpdateExpressionVisitor : ExpressionVisitor
{
    private readonly ParameterExpression _oldExpr;
    private readonly Expression _newExpr;
    public UpdateExpressionVisitor(ParameterExpression oldExpr, Expression newExpr)
    {
        _oldExpr = oldExpr;
        _newExpr = newExpr;
    }

    protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
    {
        if (node.Member.Name == _oldExpr.Name)
        {
            return node.Update(_newExpr);
        }
        Console.WriteLine(node);
        return base.VisitMemberAssignment(node);
    }
}

But I cant figure out how to alter the expression new Bar { BarInt = b.BarInt,..., to become new Bar { BarInt = f.B.BarInt,...

The function will need some kind of ExpressionVisitor like so

public class MergingVisitor : ExpressionVisitor
{
    private readonly ParameterExpression _oldExpr;
    private readonly ParameterExpression _newProp;
    private readonly ParameterExpression _newParent;
    public MergingVisitor(ParameterExpression oldExpr, ParameterExpression newProp, ParameterExpression newParent)
    {
        _oldExpr = oldExpr;
        _newProp = newProp;
        _newParent = newParent;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == _oldExpr)
        {
            /*!!what to do here!!*/
            var ma = Expression.MakeMemberAccess(_newProp, node.Member);
            return Expression.MakeMemberAccess(_newParent, ma.Member);
        }
        return base.VisitMember(node);
    }

and they all need to be linked together with something like

public static Expression<Func<T, TMap>> MapNavProperty<T, TMap, U, UMap>(this Expression<Func<T, TMap>> parent, Expression<Func<U, UMap>> nav, string propName)
{
    //concern 1 remap name of prop in nav - not sure if I should do this first
    var parentInitVarName = parent.Parameters[0].Name;
    var parentParam = Expression.Parameter(typeof(T), parentInitVarName);
    var propParam = Expression.Parameter(typeof(U), propName);
    var mergeVisitor = new MergingVisitor(nav.Parameters[0], propParam, parentParam);
    var newNavBody = mergeVisitor.Visit(nav.Body); //as MemberExpression;

    //concern 2 replace given property
    var visitor = new UpdateExpressionVisitor(propParam, nav.Body);

    return (Expression<Func<T, TMap>>)visitor.Visit(parent);
}

although at present the parentParam will fail in the Expression.MakeMemberAccess function as it is of type T (Foo in the above example), rather than type U.

How can I alter the variable name and types in the child property's lamda expression - thank you.

Update

The answers from Svick and Ivan Stoev are erudite in explanation and both work perfectly - the (passing) unit tests and code from both answers are up on GitHub here. Thank you so much to both of you - it is a shame I cannot tick 2 answers, so it comes down to Eeny, meeny, miny, moe.


回答1:


var parentParam = Expression.Parameter(typeof(T), parentInitVarName);

This won't work, parameters in expressions are not identified by name, but by reference. What you need to do is just get the parent's parameter.


var propParam = Expression.Parameter(typeof(U), propName);

This doesn't make any sense to me. propName is the name of a property, so you need to use it to create member access expression, not parameter expression.


public MergingVisitor(ParameterExpression oldExpr, ParameterExpression newProp, ParameterExpression newParent)

What you need is to replace one expression (b) with another (f.B). For that, I would create:

public class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression _oldExpr;
    private readonly Expression _newExpr;

    public ReplaceVisitor(Expression oldExpr, Expression newExpr)
    {
        _oldExpr = oldExpr;
        _newExpr = newExpr;
    }

    public override Expression Visit(Expression node)
    {
        if (node == _oldExpr)
        {
            return _newExpr;
        }
        return base.Visit(node);
    }
}

public UpdateExpressionVisitor(ParameterExpression oldExpr, Expression newExpr)

It doesn't make any sense for oldExpr to be a parameter expression. What you need is just a string.


var visitor = new UpdateExpressionVisitor(propParam, nav.Body);

You need to actually use newNavBody here.


The whole MapNavProperty() will now look like this:

var parentParam = parent.Parameters.Single();
var propExpression = Expression.Property(parentParam, propName);
var mergeVisitor = new ReplaceVisitor(nav.Parameters.Single(), propExpression);
var newNavBody = mergeVisitor.Visit(nav.Body);

var visitor = new UpdateExpressionVisitor(propName, newNavBody);

return (Expression<Func<T, TMap>>)visitor.Visit(parent);

With these changes, your code will work.




回答2:


I would personally use different helpers:

(1) For replacing parameter

static Expression ReplaceParameter(this LambdaExpression lambda, Expression target)
{
    return lambda.Body.ReplaceParameter(lambda.Parameters.Single(), target);
}

static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
{
    return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
}

class ParameterReplacer : ExpressionVisitor
{
    public ParameterExpression Source;
    public Expression Target;
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == Source ? Target : base.VisitParameter(node);
    }
}

(2) For replacing member assignment value

static Expression ReplaceMemberAssignment(this Expression expression, MemberInfo member, Expression value)
{
    return new MemberAssignmentReplacer { Member = member, Value = value }.Visit(expression);
}

class MemberAssignmentReplacer : ExpressionVisitor
{
    public MemberInfo Member;
    public Expression Value;
    protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
    {
        return node.Member == Member ? node.Update(Value) : base.VisitMemberAssignment(node);
    }
}

With these helpers, the function in question would be something like this:

public static Expression<Func<T, TMap>> MapNavProperty<T, TMap, U, UMap>(this Expression<Func<T, TMap>> parent, Expression<Func<U, UMap>> nav, string propName)
{
    var parameter = parent.Parameters[0];
    var body = parent.Body.ReplaceMemberAssignment(
        typeof(TMap).GetProperty(propName),
        nav.ReplaceParameter(Expression.Property(parameter, propName))
    );
    return Expression.Lambda<Func<T, TMap>>(body, parameter);
}


来源:https://stackoverflow.com/questions/35681045/expressiontree-method-to-assign-memberinitexpression-to-property

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!