Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I convert a lambda-expression between different (but compatible) models?

(based on an email conversation, now recorded for information sharing) I have two models used at different layers:

public class TestDTO {
    public int CustomerID { get; set; }
}
//...
public class Test {
    public int CustomerID { get; set; }
}

and a lambda in terms of my DTO layer:

Expression<Func<TestDTO, bool>> fc1 =
   (TestDTO c1) => c1.CustomerID <= 100 && c1.CustomerID >= 10;

How can I convert that lambda (in the general case) to talking about the other model:

Expression<Func<Test, bool>> fc2 = {insert magic here, based on fc1}

(obviously, we're after the same test-condition, but using the Test type)

?

like image 804
Marc Gravell Avatar asked Dec 22 '12 22:12

Marc Gravell


People also ask

What is the difference between statement lambda and expression lambda?

The difference between a statement and an expression lambda is that the statement lambda has a statement block on the right side of the lambda operator, whereas the expression lambda has only an expression (no return statement or curly braces, for example).

Does it make sense to replace lambda expression with method references?

You can't replace the lambda input -> getValueProvider(). apply(input). getValue() with a method reference without changing the semantics. A method reference replace a single method invocation, so it can't simply replace a lambda expression consisting of more than one method invocation.

Can a lambda statement have more than one statement?

The body of a statement lambda can consist of any number of statements; however, in practice there are typically no more than two or three.


2 Answers

To do that, you'll have to rebuild the expression-tree completely; the parameters will need re-mapping, and all member-access that is now talking to different types will need to be reapplied. Fortunately, a lot of this is made easier by the ExpressionVisitor class; for example (doing it all in the general case, not just the Func<T,bool> predicate usage):

class TypeConversionVisitor : ExpressionVisitor
{
    private readonly Dictionary<Expression, Expression> parameterMap;

    public TypeConversionVisitor(
        Dictionary<Expression, Expression> parameterMap)
    {
        this.parameterMap = parameterMap;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // re-map the parameter
        Expression found;
        if(!parameterMap.TryGetValue(node, out found))
            found = base.VisitParameter(node);
        return found;
    }
    protected override Expression VisitMember(MemberExpression node)
    {
        // re-perform any member-binding
        var expr = Visit(node.Expression);
        if (expr.Type != node.Type)
        {
            MemberInfo newMember = expr.Type.GetMember(node.Member.Name)
                                       .Single();
            return Expression.MakeMemberAccess(expr, newMember);
        }
        return base.VisitMember(node);
    }
}

Here, we pass in a dictionary of parameters to re-map, applying that in VisitParameter. We also, in VisitMember, check to see if we've switched type (which can happen if Visit involves a ParameterExpression or another MemberExpression, at any point): if we have, we'll try and find another member of the same name.

Next, we need a general purpose lambda-conversion rewriter method:

// allows extension to other signatures later...
private static Expression<TTo> ConvertImpl<TFrom, TTo>(Expression<TFrom> from)
    where TFrom : class
    where TTo : class
{
    // figure out which types are different in the function-signature
    var fromTypes = from.Type.GetGenericArguments();
    var toTypes = typeof(TTo).GetGenericArguments();
    if (fromTypes.Length != toTypes.Length)
        throw new NotSupportedException(
            "Incompatible lambda function-type signatures");
    Dictionary<Type, Type> typeMap = new Dictionary<Type,Type>();
    for (int i = 0; i < fromTypes.Length; i++)
    {
        if (fromTypes[i] != toTypes[i])
            typeMap[fromTypes[i]] = toTypes[i];
    }

    // re-map all parameters that involve different types
    Dictionary<Expression, Expression> parameterMap
        = new Dictionary<Expression, Expression>();
    ParameterExpression[] newParams =
        new ParameterExpression[from.Parameters.Count];
    for (int i = 0; i < newParams.Length; i++)
    {
        Type newType;
        if(typeMap.TryGetValue(from.Parameters[i].Type, out newType))
        {
            parameterMap[from.Parameters[i]] = newParams[i] =
                Expression.Parameter(newType, from.Parameters[i].Name);
        }
        else
        {
            newParams[i] = from.Parameters[i];
        }
    }

    // rebuild the lambda
    var body = new TypeConversionVisitor(parameterMap).Visit(from.Body);
    return Expression.Lambda<TTo>(body, newParams);
}

This takes an arbitrary Expression<TFrom>, and a TTo, converting it to an Expression<TTo>, by:

  • finding which types are different between TFrom / TTo
  • using that to re-map the parameters
  • using the expression-visitor we just created
  • and finally constructing a new lambda expression for the desired signature

Then, putting it all together and exposing our extension method:

public static class Helpers {
    public static Expression<Func<TTo, bool>> Convert<TFrom, TTo>(
        this Expression<Func<TFrom, bool>> from)
    {
        return ConvertImpl<Func<TFrom, bool>, Func<TTo, bool>>(from);
    }

    // insert from above: ConvertImpl
    // insert from above: TypeConversionVisitor
}

et voila; a general-purpose lambda conversion routine, with a specific implementation of:

Expression<Func<Test, bool>> fc2 = fc1.Convert<TestDTO, Test>();
like image 170
Marc Gravell Avatar answered Oct 11 '22 12:10

Marc Gravell


You could use AutoMapper (no expression tree):

Mapper.CreateMap<Test, TestDTO>();

...

Func<TestDTO, bool> fc1 =
  (TestDTO c1) => c1.CustomerID <= 100 && c1.CustomerID >= 10;

Func<Test, bool> fc2 =
  (Test t) => fc1(Mapper.Map<Test, TestDTO>(t));
like image 22
Jordão Avatar answered Oct 11 '22 13:10

Jordão