If I make any mistakes/mistypes in the following code, please don't get irrate, just drop a comment on here and I'll fix immediately - thanks
Goal
Re-map Expression<TDelegate>
from one EntityA
to a EntityB
.
I suspect this kind of thing has been done before but I haven't found any particularly useful links so feel free to point me in the right direction.
What I have so far is a selection of classes that combine to allow for mappings to be created between Entity Members on two given classes. As an example, the surface API might have the following signature:
public void AddMemberBinding<TEntityA, TEntityB, TMember>(Func<TEntityA, TMember> entityAMemberSelector, Func<TEntityB, TMember> entityBMemberSelector)
{
// does some magic, eventually storing the necessary MemberInfo details required to
// "remap" MemberExpressions (MemberAccess) from TEntityA to TEntityB
}
Given the following classes...
public class EntityA
{
public long Id { get; set; }
public string Name { get; set ;}
}
public class EntityB
{
public long MyId { get; set; }
public string MyName { get; set; }
}
You would be able to create bindings with something along the lines of...
public static void AddBindings()
{
AddMemberBinding((EntityA n) => n.Id, (EntityB n) => n.MyId);
AddMemberBinding((EntityA n) => n.Name, (EntityB n) => n.MyName);
}
In my case, I have Assembly1
that knows what EntityA
is, but does not know EntityB
. I have Assembly2
which knows what both EntityA
and EntityB
are, and is visible to Assembly1
. Assembly2
provides a method to Assembly1
which might look as follows:
public static IEnumerable<EntityA> GetData<TResult>(Expression<Func<EntityA, bool>> criteria, Expression<Func<EntityA, TResult>> selector)
{
// EntityB's are stored in a database, I could do one of two things here...
// 1) Return all EntitieB's and then apply criteria and selector through the IEnumerable extensions
// this would be sub-optimal - particularly if there are millions of EntityB's!
// 2) "Transmute" (for lack of a better word) the expressions provided, using the keymappings
// specified earlier, to derive expressions that can be passed through to the QueryableProvider
// ... as you might have guessed, I opted for #2
}
I'm using a derived version of the ExpressionTree Visitor, with the following overridden methods:
protected override Expression VisitLambda(LambdaExpression lambda)
{
Type targetParameterType = lambda.Parameters[0].Type;
Type targetExpressionType = lambda.Type;
If (lambda.Parameters.Count = 1 && lambda.Parameters(0).Type == EntityA)
{
targetParameterType = EntityB;
// the `GetResultType` method called gets the TResult type from Func<T, TResult>
Type targetExpressionResultType = GetResultType(lambda);
targetExpressionType = gettype(Func<EntityB, targetExpressionResultType>)
}
// this is probably wrong, but maintains the current (last) parameter instance
// I started doing this after reading about a similar issue to mine found:
// https://stackoverflow.com/questions/411738/expression-or-the-parameter-item-is-not-in-scope
this.CurrentLambdaParameters = lambda.Parameters.Select(x => Expression.Parameter(targetParameterType, x.Name));
Expression body = this.Visit(lambda.Body);
If (body != lambda.Body)
{
return Expression.Lambda(targetExpressionType, body, this.CurrentLambdaParameters);
}
return lambda;
}
protected override Expression VisitMemberAccess(MemberExpression m)
{
// at this point I go off and look at key mappings, fetch the mapping required, etc
// the entity I retrieve has a `TargetMemberInfo` property which is a `MemberInfo` for the
// member on the target entity - speaks for itself I guess...
return Expression.MakeMemberAccess(this.CurrentParameters.Single(), myMappingClassThing.TargetMemberInfo);
}
The Problem
With all that said and done, when I run through the code with a test case, I get the error in the title... I can see it's a parameter issue from the description, but having read about a similiar issue I had hoped I had accounted for the problem in the VisitMemberAccess
method by using the parameter I created when modifying the root lambda expression - same instance of ParameterExpression fixed the linked questions problem I think?
It seems I didn't understand that part of the process very well. The question is, where did I go wrong!? What is it I need to do with these ParameterExpressions for them to be ""in scope""?
Thanks in advance for your answers, and if you read this far, kudos to you!!
Whilst looking over Jon's remarkably similiar question and refactoring to incorporate a couple of his practices I preferred to my own implementation, I stumbled across the answer. I noticed that
VisitParameter
was never called, the reason for which is that my override ofVisitMemberAccess
discontinued the recursion through the expression tree.It should have looked like (using a different overload):
Combine that with ensuring you don't create multiple instances of the same parameter and everything slots together nicely.
Thanks again to Jon! =)