Translating expression tree from a type to another type with complex mappings

asp.net-web-api, entity-framework, expression-trees, linq

Solution

I took the liberty of modifying your code just a hair but this does the trick,

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Id { get; set; }
    public DateTime? BirthDate { get; set; }
    public int CustomerTypeId { get; set; }
}

public class CustomerModel
{
    public string FullName { get; set; }
    public bool HasEvenId { get; set; }
}

sealed class AToBConverter<TA, TB> : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> _parameters = new Dictionary<ParameterExpression, ParameterExpression>();
    private readonly Dictionary<MemberInfo, LambdaExpression> _mappings;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof(TA))
        {
            ParameterExpression parameter;
            if (!this._parameters.TryGetValue(node, out parameter))
            {
                this._parameters.Add(node, parameter = Expression.Parameter(typeof(TB), node.Name));
            }
            return parameter;
        }
        return node;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == null || node.Expression.Type != typeof(TA))
        {
            return base.VisitMember(node);
        }
        Expression expression = this.Visit(node.Expression);
        if (expression.Type != typeof(TB))
        {
            throw new Exception("Whoops");
        }
        LambdaExpression lambdaExpression;
        if (this._mappings.TryGetValue(node.Member, out lambdaExpression))
        {
            return new SimpleExpressionReplacer(lambdaExpression.Parameters.Single(), expression).Visit(lambdaExpression.Body);
        }
        return Expression.Property(
            expression,
            node.Member.Name
        );
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        return Expression.Lambda(
            this.Visit(node.Body),
            node.Parameters.Select(this.Visit).Cast<ParameterExpression>()
        );
    }

    public AToBConverter(Dictionary<MemberInfo, LambdaExpression> mappings)
    {
        this._mappings = mappings;
    }
}

sealed class SimpleExpressionReplacer : ExpressionVisitor
{
    private readonly Expression _replacement;
    private readonly Expression _toFind;

    public override Expression Visit(Expression node)
    {
        return node == this._toFind ? this._replacement : base.Visit(node);
    }

    public SimpleExpressionReplacer(Expression toFind, Expression replacement)
    {
        this._toFind = toFind;
        this._replacement = replacement;
    }
}

class Program 
{
    private static Dictionary<MemberInfo, LambdaExpression> GetMappings()
    {
        var mappings = new Dictionary<MemberInfo, LambdaExpression>();
        var mapping = GetMappingFor(model => model.HasEvenId, customer => (customer.Id % 2) == 0);
        mappings.Add(mapping.Item1, mapping.Item2);
        mapping = GetMappingFor(model => model.FullName, customer => customer.FirstName + " " + customer.LastName);
        mappings.Add(mapping.Item1, mapping.Item2);
        return mappings;
    }

    private static Tuple<MemberInfo, LambdaExpression> GetMappingFor<TValue>(Expression<Func<CustomerModel, TValue>> fromExpression, Expression<Func<Customer, TValue>> toExpression)
    {
        return Tuple.Create(((MemberExpression)fromExpression.Body).Member, (LambdaExpression)toExpression);
    }

    static void Main()
    {
        Expression<Func<CustomerModel, bool>> source = model => model.HasEvenId && model.FullName == "John Smith";
        Expression<Func<Customer, bool>> desiredResult = model => (model.Id % 2) == 0 && (model.FirstName + " " + model.LastName) == "John Smith";
        Expression output = new AToBConverter<CustomerModel, Customer>(GetMappings()).Visit(source);
        Console.WriteLine("The two expressions do {0}match.", desiredResult.ToString() == output.ToString() ? null : "not ");
        Console.ReadLine();
    }
}

Problem

inspired by this answer I'm trying to map a property on a model class to an expression based on the actual entity. These are the two classes involved: ``` public class Customer { public string FirstName { get; set; } public string LastName { get; set; } public int Id { get; set; } public DateTime? BirthDate { get; set; } public int CustomerTypeId { get; set; } } public class CustomerModel { ... public bool HasEvenId { get; set; } } ``` An example of a possible expression I'd like to convert is: ``` Expression<Func<CustomerModel, bool>> from = model => model.HasEvenId; Expression<Func<Customer, bool>> to = entity => ((entity.Id % 2) == 0); ``` The problem is that I have to expose an OData endpoint via ASP.NET WebAPI but I need to make some operations on the entities before I can them, hence the need of a model class and the need to translate the expression based on the model that I could receive as an OData query in an expression based on the entity, that I would use to query EF4. This is where I got so far: ``` private static readonly Dictionary<Expression, Expression> Mappings = GetMappings(); private static Dictionary<Expression, Expression> GetMappings() { var mappings = new Dictionary<Expression, Expression>(); var mapping = GetMappingFor((CustomerModel model) => model.HasEvenId, (Customer customer) => (customer.Id%2) == 0); mappings.Add(mapping.Item1, mapping.Item2); return mappings; } private static Tuple<Expression, Expression> GetMappingFor<TFrom, TTo, TValue>(Expression<Func<TFrom, TValue>> fromExpression, Expression<Func<TTo, TValue>> toExpression) { MemberExpression fromMemberExpression = (MemberExpression) fromExpression.Body; return Tuple.Create<Expression, Expression>(fromMemberExpression, toExpression); } public static Expression<Func<TTo, bool>> Translate<TFrom, TTo>(Expression<Func<TFrom, bool>> expression, Dictionary<Expression, Expression> mappings = null) { if (expression == null) return null; string parameterName = expression.Parameters[0].Name; parameterName = string.IsNullOrWhiteSpace(parameterName) ? "p" : parameterName; var param = Expression.Parameter(typeof(TTo), parameterName); var subst = new Dictionary<Expression, Expression> { { expression.Parameters[0], param } }; ParameterChangeVisitor parameterChange = new ParameterChangeVisitor(parameterName); if (mappings != null) foreach (var mapp in mappings) subst.Add(mapp.Key, parameterChange.Visit(mapp.Value)); var visitor = new TypeChangeVisitor(typeof(TFrom), typeof(TTo), subst); return Expression.Lambda<Func<TTo, bool>>(visitor.Visit(expression.Body), param); } public IQueryable<CustomerModel> Get() { var filterExtractor = new ODataFilterExtractor<CustomerModel>(); Expression<Func<CustomerModel, bool>> expression = filterExtractor.Extract(Request); Expression<Func<Customer, bool>> translatedExpression = Translate<CustomerModel, Customer>(expression, Mappings); IQueryable<Customer> query = _context.Customers; if (translatedExpression != null) query = query.Where(translatedExpression); var finalQuery = from item in query.AsEnumerable() select new CustomerModel() { FirstName = item.FirstName, LastName = item.LastName, Id = item.Id, BirthDate = item.BirthDate, CustomerTypeId = item.CustomerTypeId, HasEvenId = (item.Id % 2 ) == 0 }; return finalQuery.AsQueryable(); } ``` where: - ODataFilterExtractor is a class that extract the $filter expression from the RequestMessage we receive; - ParameterChangeVisitor just changes all the ParameterExpression to a new one having the provided string as parameter name; In addition, I changed the VisitMember method of the answer linked above in this way: ``` protected override Expression VisitMember(MemberExpression node) { // if we see x.Name on the old type, substitute for new type if (node.Member.DeclaringType == _from) { MemberInfo toMember = _to.GetMember(node.Member.Name, node.Member.MemberType, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).SingleOrDefault(); if (toMember != null) { return Expression.MakeMemberAccess(Visit(node.Expression), toMember); } else { if (_substitutions.Select(kvp => kvp.Key).OfType<MemberExpression>().Any(me => me.Member.Equals(node.Member))) { MemberExpression key = _substitutions.Select(kvp => kvp.Key).OfType<MemberExpression>().Single(me => me.Member.Equals(node.Member)); Expression value = _substitutions[key]; // What to return here? return Expression.Invoke(value); } } } return base.VisitMember(node); } ``` Thanks for you help.

Original source

Related problems