问题
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