Reputation: 1771
I'm trying to filter data based on the CodeProject article here, but I'm having difficulty with values that aren't strings. Here's what I'm doing right now:
public static class ExpressionBuilder
{
private static readonly MethodInfo containsMethod = typeof(string).GetMethod("Contains");
private static readonly MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) });
private static readonly MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) });
private static Expression GetExpression<T>(ParameterExpression param, Filter filter)
{
//the property that I access here is stored as a string
MemberExpression member = Expression.Property(param, filter.PropertyName);
//this value is brought in from a web request as a string, but could be a numeric, datetime, or other type
ConstantExpression constant = Expression.Constant(filter.Value);
try
{
decimal numericValue = 0;
if (decimal.TryParse(filter.Value.ToString(), out numericValue))
{
var numericConstant = Expression.Constant(numericValue);
var numericProperty = ConvertToType(param, (PropertyInfo)member.Member, TypeCode.Decimal);
switch (filter.Operation)
{
case Enums.Op.Equals:
return Expression.Equal(member, numericConstant);
case Enums.Op.NotEqual:
return Expression.NotEqual(member, numericConstant);
case Enums.Op.Contains:
return Expression.Call(member, containsMethod, constant);
case Enums.Op.StartsWith:
return Expression.Call(member, startsWithMethod, constant);
case Enums.Op.EndsWith:
return Expression.Call(member, endsWithMethod, constant);
case Enums.Op.GreaterThan:
return Expression.GreaterThan(numericProperty, numericConstant);
}
}
else
{
//this part works fine for values that are simple strings.
switch (filter.Operation)
{
case Enums.Op.Equals:
return Expression.Equal(member, constant);
case Enums.Op.NotEqual:
return Expression.NotEqual(member, constant);
case Enums.Op.Contains:
return Expression.Call(member, containsMethod, constant);
case Enums.Op.StartsWith:
return Expression.Call(member, startsWithMethod, constant);
case Enums.Op.EndsWith:
return Expression.Call(member, endsWithMethod, constant);
case Enums.Op.GreaterThan:
return Expression.GreaterThan(member, constant);
}
}
}
return null;
}
private static MethodCallExpression ConvertToType(ParameterExpression source, PropertyInfo sourceProperty, TypeCode type)
{
var sourceExProperty = Expression.Property(source, sourceProperty);
var typeChangeMethod = typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(TypeCode) });
var returner = Expression.Call(typeChangeMethod, sourceExProperty, Expression.Constant(type));
return returner;
}
}
public class Filter
{
public string PropertyName { get; set; }
public int Key { get; set; }
public string Operator { get; set; }
public Enums.Op Operation
{
get
{
switch (Operator.Trim())
{
case "<=":
return Enums.Op.LessThanOrEqual;
case ">=":
return Enums.Op.GreaterThanOrEqual;
case "=":
return Enums.Op.Equals;
case "<":
return Enums.Op.LessThan;
case ">":
return Enums.Op.GreaterThan;
case "not equal to":
return Enums.Op.NotEqual;
case "contains":
return Enums.Op.Contains;
case "starts with":
return Enums.Op.StartsWith;
case "ends with":
return Enums.Op.EndsWith;
default:
return new Enums.Op();
}
}
}
public object Value { get; set; }
}
When I try to filter based on a numeric, eg (value < 12), I get an exception that you cannot compare an Object to a decimal. Is ConvertToType not actually converting the value from a string to a decimal?
Thanks in advance!
Upvotes: 1
Views: 1620
Reputation: 7478
The easiest way in your case is to treat filter.Value
as the same object type as in property, and then you can make like this for
var propInfo = (PropertyInfo)member.Member;
var constantValue = Expression.Constant(filter.Value, propInfo.PropertyType);
var typeCode = Type.GetTypeCode(propInfo.PropertyType);
var property = Expression.Property(param, propInfo);
switch (filter.Operation)
{
case Enums.Op.Equals:
return Expression.Equal(property, constantValue);
case Enums.Op.NotEqual:
return Expression.NotEqual(property, constantValue);
case Enums.Op.GreaterThan:
return Expression.GreaterThan(property, constantValue);
}
And in that case it would work not for just specific decimal type, but for int, string and other simple types too. And it also would depend on your allowed operations.
So idea is to hold actual type in filter.Value
the same as in property type, so it will just generate pure compari
UPDATE
Here is another sample based on your comment. New sample will do like x.Prop1 == 42
(42 is Filter.value) if they have the same type, or it will do strange conversion (decimal)(Convert.ChangeType((object)x.Prop1, typeof(decimal))) == (decimal)(Convert.ChangeType((object)42, typeof(decimal)))
, so it will try to convert any type in property and filter.Value to specified as <T>
for method call and then use it for conversion. It may produce unneeded boxing operations, and probably can be optimzed a bit, but if this isn't often operation it shouldn't matter.
internal class Program
{
private static void Main(string[] args)
{
Filter filter = new Filter();
filter.Operator = ">";
filter.Value = "42";
filter.PropertyName = "Prop1";
ParameterExpression expr = Expression.Parameter(typeof (Test), "x");
var resultExpr = ExpressionBuilder.GetExpression<decimal>(expr, filter);
// and it will hold such expression:
// {(Convert(x.Prop1) > Convert(ChangeType(Convert("42"), System.Decimal)))}
Console.WriteLine(expr.ToString());
}
}
public class Test
{
public int Prop1 { get; set; }
}
public static class ExpressionBuilder
{
private static readonly MethodInfo containsMethod = typeof (string).GetMethod("Contains");
private static readonly MethodInfo startsWithMethod = typeof (string).GetMethod("StartsWith",
new Type[] {typeof (string)});
private static readonly MethodInfo endsWithMethod = typeof (string).GetMethod("EndsWith", new Type[] {typeof (string)});
private static readonly MethodInfo changeTypeMethod = typeof (Convert).GetMethod("ChangeType",
new Type[] {typeof (object), typeof (Type)});
public static Expression GetExpression<T>(ParameterExpression param, Filter filter)
{
//the property that I access here is stored as a string
MemberExpression member = Expression.Property(param, filter.PropertyName);
//this value is brought in from a web request as a string, but could be a numeric, datetime, or other type
ConstantExpression constant = Expression.Constant(filter.Value);
Expression targetValue;
Expression sourceValue;
if (filter.Value != null && member.Type == filter.Value.GetType() && member.Type == typeof(T))
{
targetValue = constant;
sourceValue = member;
}
else
{
var targetType = Expression.Constant(typeof(T));
targetValue = Convert(constant, typeof(object));
sourceValue = Convert(member, typeof(object));
targetValue = Expression.Call(changeTypeMethod, targetValue, targetType);
sourceValue = Expression.Call(changeTypeMethod, sourceValue, targetType);
targetValue = Convert(targetValue, member.Type);
sourceValue = Convert(member, member.Type);
}
try
{
switch (filter.Operation)
{
case Enums.Op.Equals:
return Expression.Equal(sourceValue, targetValue);
case Enums.Op.NotEqual:
return Expression.NotEqual(sourceValue, targetValue);
case Enums.Op.Contains:
return Expression.Call(sourceValue, containsMethod, targetValue);
case Enums.Op.StartsWith:
return Expression.Call(sourceValue, startsWithMethod, targetValue);
case Enums.Op.EndsWith:
return Expression.Call(sourceValue, endsWithMethod, targetValue);
case Enums.Op.GreaterThan:
return Expression.GreaterThan(sourceValue, targetValue);
}
}
catch (Exception ex)
{
throw;
}
return null;
}
private static Expression Convert(Expression from, Type type)
{
return Expression.Convert(from, type);
}
}
public class Filter
{
public string PropertyName { get; set; }
public int Key { get; set; }
public string Operator { get; set; }
public Enums.Op Operation
{
get
{
switch (Operator.Trim())
{
case "<=":
return Enums.Op.LessThanOrEqual;
case ">=":
return Enums.Op.GreaterThanOrEqual;
case "=":
return Enums.Op.Equals;
case "<":
return Enums.Op.LessThan;
case ">":
return Enums.Op.GreaterThan;
case "not equal to":
return Enums.Op.NotEqual;
case "contains":
return Enums.Op.Contains;
case "starts with":
return Enums.Op.StartsWith;
case "ends with":
return Enums.Op.EndsWith;
default:
return new Enums.Op();
}
}
}
public object Value { get; set; }
}
public class Enums
{
public enum Op
{
LessThanOrEqual,
GreaterThanOrEqual,
Equals,
LessThan,
GreaterThan,
NotEqual,
Contains,
StartsWith,
EndsWith
}
}
Upvotes: 2