5

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.

Community
  • 1
  • 1
Kralizek
  • 1,999
  • 1
  • 28
  • 47
  • Well, you are really brave! Implementing a Linq provider all be yourself... – Aliostad May 13 '12 at 12:21
  • Could you convert your woolly sentence _The problem is (...) to query EF4._ to a clear, succinct question? Possibly mark the piece of code where you need help? – Gert Arnold May 13 '12 at 13:02
  • i think a visitor could do the job – Proviste May 13 '12 at 14:08
  • The clear question would be: given an expression based on a certain class, how can I get an equivalent expression working on a second class with translations more complex than a simple mapping by name? I hope this can help. – Kralizek May 13 '12 at 18:09
  • What about making an SQL View? – Guillaume86 May 14 '12 at 16:17
  • I was looking for a generic solution. The fact that the query is going to be fired on the database is just a particular scenario. In addition, the query I need to run isn't just SQL because some computation is required from the output of an SQL query. – Kralizek May 15 '12 at 06:40

2 Answers2

5

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();
    }
}
Double Down
  • 898
  • 6
  • 13
  • As you can see in the code, I added a FullName property to the model class just to test the relationship mapping even further. I also took the liberty in the visitor of coding it such that any other property will exist in both (i.e. if you added an Id property to the CustomerModel, then any query on CustomerModel Id property would simply be translated as a query on Customer Id since there is no mapping specifically for that property) – Double Down May 18 '12 at 20:35
  • Very many thanks for this. By combining your code with the code from http://stackoverflow.com/questions/18365764/implement-iqueryable-wrapper-to-translate-result-objects, I was able to provide an OData layer on top of a massive pre-existing database. It took a little extra work to translate IQueryable objects built from repository pattern classes and pass them to OData, but it works. Using traditional projection and/or `.ToList()` calls resulted in OOM errors.Your approach allows very-late binding so that objects are only translated when OData requests them specifically. – user326608 Oct 28 '15 at 09:40
  • Any advice on how to genercise the input literals? I.e., in the full name example, what if you didn't know what "John Smith" would be, but could be any string value? – NStuke Jul 13 '16 at 22:04
3

Another solution would be to use AutoMapper to map complex types and modify the resulting expression query with an ExpressionTransformer before it gets executed. I will try to explain with a complete sample:

Model classes

Some POCO classes just for holding data.

public enum CostUnitType
{
    None = 0,
    StockUnit = 1,
    MachineUnit = 2,
    MaintenanceUnit = 3
}

public class CostUnit
{
    public string CostUnitId { get; set; }
    public string WorkplaceId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public CostUnitType Type { get; set; }

    public override string ToString()
    {
        return CostUnitId + " " + Name + " " + Type;
    }
}

public class StockUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MachineUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MaintenanceUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

The ExpressionTransformer class

Most of the time, using the Mapper static class is fine, but sometimes you need to map the same types with different configurations, so you need to explicitly use an IMappingEngine like this:

var configuration = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
var engine = new MappingEngine(configuration);

The way to create a MappingEngine is not obvious at all. I had to dig in the source code to find how it was done.

public static class ExpressionTransformer
{
    private static readonly MappingEngine Mapper;

    /// <summary>
    /// Initializes the <see cref="ExpressionTransformer"/> class.
    /// Creates an instance of AutoMapper. Initializes mappings.
    /// </summary>
    static ExpressionTransformer()
    {
        ConfigurationStore configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);

        // Create mapping
        // Maps Id from StockUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<StockUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));
        // Maps Id from MachineUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<MachineUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));
        // Maps Id from MaintenanceUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<MaintenanceUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));

        // Create instance of AutoMapper
        Mapper = new MappingEngine(configurationStore);
    }

    public static Expression<Func<TDestination, bool>> Tranform<TSource, TDestination>(Expression<Func<TSource, bool>> sourceExpression)
    {
        // Resolve mappings by AutoMapper for given types.
        var map = Mapper.ConfigurationProvider.FindTypeMapFor(typeof(TSource), typeof(TDestination));

        if (map == null)
        {
            throw new AutoMapperMappingException(string.Format("No Mapping found for {0} --> {1}.", typeof(TSource).Name, typeof(TDestination).Name));
        }

        // Transform from TSource to TDestination with specified mappings
        var visitor = new ParameterTypeVisitor<TSource, TDestination>(sourceExpression, map.GetPropertyMaps());
        var expression = visitor.Transform();

        return expression;
    }

    private class ParameterTypeVisitor<TSource, TDestination> : ExpressionVisitor
    {
        private readonly Dictionary<string, ParameterExpression> _parameters;
        private readonly Expression<Func<TSource, bool>> _expression;
        private readonly IEnumerable<PropertyMap> _maps;

        public ParameterTypeVisitor(Expression<Func<TSource, bool>> expression, IEnumerable<PropertyMap> maps)
        {
            _parameters = expression.Parameters
                .ToDictionary(p => p.Name, p => Expression.Parameter(typeof(TDestination), p.Name));

            _expression = expression;

            _maps = maps;
        }

        public Expression<Func<TDestination, bool>> Transform()
        {
            return (Expression<Func<TDestination, bool>>) Visit(_expression);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Member.DeclaringType == typeof(TSource))
            {
                var memberName = node.Member.Name;
                var member = _maps.FirstOrDefault(p => typeof(TSource) == node.Expression.Type
                                                        && p.SourceMember.Name == memberName);
                if (member != null)
                {
                    // Return Property from TDestination
                    var expression = Visit(node.Expression);
                    return Expression.MakeMemberAccess(expression, member.DestinationProperty.MemberInfo);
                }
            }

            return base.VisitMember(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var parameter = _parameters[node.Name];
            return parameter;
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            var expression = Visit(node.Body);
            return Expression.Lambda(expression, _parameters.Select(x => x.Value));
        }
    }
}

Usage

To Convert an expression we just need to call Transform Method of ExpressionTransformer class

            Expression<Func<StockUnit, bool>> stockQuery = unit => unit.Id == "0815" && unit.Name == "ABC";

            // Call Transform<TSource, TDestination> method.
            Expression<Func<CostUnit, bool>> costUnitQuery = ExpressionTransformer.Tranform<StockUnit, CostUnit>(stockQuery);

Result

Resulting expressions

Sneijder
  • 319
  • 1
  • 11