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.
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...