How to replace property type and its value in expression trees

c# expression-trees

Question

I have a class PersonDTO with Nullable DateTime property:

public class PersonDTO
{
    public virtual long Id { get; set; }
    public virtual string Name { get; set; }
    // YYYYMMDD format
    public virtual Nullable<int> Birthday { get; set; }
}

And a class in Presentation layer:

public class PersonViewModel
{
    public virtual long Id { get; set; }
    public virtual string Name { get; set; }
    public virtual Nullable<DateTime> Birthday { get; set; }
}

On my Form I have two methods which are responsible for creating Expression<Func<PersonViewModel, bool>> object:

    private Expression<Func<PersonViewModel, bool>> GetFilterExpression()
    {             
        Expression condition = null;
        ParameterExpression pePerson = Expression.Parameter(typeof(PersonViewModel), "person");
        //...
        if (dtpBirth.Format != DateTimePickerFormat.Custom)
        {
            Expression target = Expression.Property(pePerson, pePerson.Type.GetProperty("Birthday", typeof(DateTime?)));
            UnaryExpression date = Expression.Convert(Expression.Constant(dtpBirth.Value.Date), typeof (DateTime?));
            condition = (condition == null)
                    ? Expression.GreaterThan(target, date)
                    : Expression.And(condition, Expression.GreaterThan(target, date));
        }
        // Формируем лямбду с условием и возвращаем результат сформированного фильтра
        return condition != null ? Expression.Lambda<Func<PersonViewModel, bool>>(condition, pePerson) : null;
    }

Also I'm using AutoMapper? which converts one Expression<Func<PersonViewModel, bool>> to Expression<Func<PersonDTO, bool>>. The code for conversion looks like:

// ...
Mapper.CreateMap<PersonViewModel, PersonDTO>()
              .ForMember(dto => dto.Birthday, opt => opt.MapFrom(model => model.BirthdaySingle.NullDateTimeToNullInt("yyyyMMdd")));
// ...
public static class DataTypesExtensions
{
    public static DateTime? NullIntToNullDateTime(this int? input, string format)
    {
        if (input.HasValue)
        {
            DateTime result;
            if (DateTime.TryParseExact(input.Value.ToString(), format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
            {
                return result;
            }
        }
        return null;
    }

    //...
}

My Expression converter looks like:

    public static Expression<Func<TDestination, TResult>> RemapForType<TSource, TDestination, TResult>(
        this Expression<Func<TSource, TResult>> expression)
    {
        var newParameter = Expression.Parameter(typeof(TDestination));

        var visitor = new AutoMapVisitor<TSource, TDestination>(newParameter);
        var remappedBody = visitor.Visit(expression.Body);
        if (remappedBody == null)
        {
            throw new InvalidOperationException("Unable to remap expression");
        }

        return Expression.Lambda<Func<TDestination, TResult>>(remappedBody, newParameter);
    }

public class AutoMapVisitor<TSource, TDestination> : ExpressionVisitor
{
    private readonly ParameterExpression _newParameter;
    private readonly TypeMap _typeMap = Mapper.FindTypeMapFor<TSource, TDestination>();

    public AutoMapVisitor(ParameterExpression newParameter)
    {
        _newParameter = newParameter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var propertyMaps = _typeMap.GetPropertyMaps();

        // Find any mapping for this member
        // Here I think is a problem, because if it comes (person.Birthday => Convert(16.11.2016 00:00:00)) it can't find it.
        var propertyMap = propertyMaps.SingleOrDefault(map => map.SourceMember == node.Member);
        if (propertyMap == null)
        {
            return base.VisitMember(node);
        }

        var destinationProperty = propertyMap.DestinationProperty;
        var destinationMember = destinationProperty.MemberInfo;

        // Check the new member is a property too
        var property = destinationMember as PropertyInfo;
        if (property == null)
        {
            return base.VisitMember(node);
        }

        // Access the new property
        var newPropertyAccess = Expression.Property(_newParameter, property);
        return base.VisitMember(newPropertyAccess);
    }
}

I need somehow to convert part of a lambda expression: person => person.Birthday > Convert(15.11.2016 00:00) (in this case person is PersonViewModel and Birthday of type DateTime?) to something look like: person => person.Birthday > 20161115 (in this case person is PersonDTO and Birthday of type int?). Without this issue everything maps and works correctly. I understand that I need to go deeper into the tree and doing some manipulation, but I can't understand how and where should I do this.

Popular Answer

I would adapt the value of the datetime on binary expressions with sg along:

class AutoMapVisitor<TSource, TDestination>: ExpressionVisitor
{
    // your stuff
    protected override Expression VisitBinary(BinaryExpression node)
    {
        var memberNode = IsBirthdayNode(node.Left)
            ? node.Left
            : IsBirthdayNode(node.Right)
                ? node.Right
                : null;
        if (memberNode != null)
        {
            var valueNode = memberNode == node.Left
                ? node.Right
                : node.Left;
            // get the value
            var valueToChange = (int?)getValueFromNode(valueNode);
            var leftIsMember = memberNode == node.Left;
            var newValue = Expression.Constant(DataTypesExtensions.NullIntToNullDateTime(valueToChange, /*insert your format here */ ""));
            var newMember = Visit(memberNode);
            return Expression.MakeBinary(node.NodeType, leftIsMember ? newMember : newValue, leftIsMember ? newValue : newMember); // extend this if you have a special comparer or sg
        }
        return base.VisitBinary(node);
    }

    private bool IsBirthdayNode(Expression ex)
    {
        var memberEx = ex as MemberExpression;
        return memberEx != null && memberEx.Member.Name == "Birthday" && memberEx.Member.DeclaringType == typeof(PersonViewModel);
    }

    private object getValueFromNode(Expression ex)
    {
        var constant = ex as ConstantExpression;
        if (constant != null)
            return constant.Value;
        var cast = ex as UnaryExpression;
        if (cast != null && ex.NodeType == ExpressionType.Convert)
            return getValueFromNode(cast.Operand);
        // here you can add more shortcuts to improve the performance of the worst case scenario, which is:
        return Expression.Lambda(ex).Compile().DynamicInvoke(); // this will throw an exception, if you have references to other parameters in your ex
    }

}

it is quite specific, but you get the idea, and you can make it more generic for your usecases.

But I think your mapping for the property is wrong. In sql you want to use int comparison. The above does that for you. When automapper changes your properties, it should just replace the old birthday, with the new one (changing the type), without the call to NullDateTimeToNullInt. The above code will take care of the type change for comparisons. If you have the member in anonymous selects or other places, you will still have a problem I believe...



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why