I am trying to build generic sort method using Expressions.
I came up with the following method. For some reasons the code is breaking at the statement when a nested property is used in the sort expression.
var sortExpression = Expression.Lambda<Func<T, object>>
(Expression.Convert(Expression.Property(sortParam, sortColumn), typeof(object)), sortParam);
private static IQueryable<T> SortQuery<T>(IQueryable<T> query, string sortColumn)
{
if (!string.IsNullOrEmpty(sortColumn))
{
var sortParam = Expression.Parameter(typeof(T), "x");
Expression expr = sortParam;
foreach (var prop in sortColumn.Split('.'))
{
expr = Expression.PropertyOrField(expr, prop);
}
var sortExpression = Expression.Lambda<Func<T, object>>
(Expression.Convert(Expression.Property(sortParam, sortColumn), typeof(object)), sortParam);
return query.OrderBy(sortExpression);
}
return null;
}
Any idea where I am doing it wrong ?
Try:
private static readonly MethodInfo OrderBy = (from x in typeof(Queryable).GetMethods()
where x.Name == "OrderBy"
let pars = x.GetParameters()
where pars.Length == 2
select x).Single();
private static IQueryable<T> SortQuery<T>(IQueryable<T> query, string sortColumn)
{
if (!string.IsNullOrEmpty(sortColumn))
{
var sortParam = Expression.Parameter(typeof(T), "x");
Expression expr = sortParam;
foreach (var prop in sortColumn.Split('.'))
{
expr = Expression.PropertyOrField(expr, prop);
}
var lambda = Expression.Lambda(expr, sortParam);
var orderBy = OrderBy.MakeGenericMethod(typeof(T), expr.Type);
return (IQueryable<T>)orderBy.Invoke(null, new object[] { query, lambda });
}
return null;
}
You have to use reflection to invoke the Queryable.OrderBy. In general, casting to object doesn't work very well with Entity Framework/LINQ to SQL, so you can try it, but there is only a very little chance it will work.
Related
I know how to build an Expression tree to query Entity Framework, eg.
string propertyName = "Name";
string keyword = "Methanol";
ParameterExpression parameter = Expression.Parameter(typeof(Chemical), "x");
MemberExpression me = Expression.Property(parameter, propertyName);
ConstantExpression constant = Expression.Constant(keyword, typeof(string));
BinaryExpression body = Expression.Equal(me, constant);
var isEqualExpressionTree = Expression.Lambda<Func<Chemical, bool>>(body, new[] { parameter });
Expression<Func<Chemical, bool>> funcExpression = (Expression<Func<Chemical, bool>>)isEqualExpressionTree;
using (var mLEntities = new myLab02Entities1())
{
var cl = mLEntities.Chemicals.AsQueryable().Where(funcExpression).ToList();
return cl;
}
"Name" is a field in the SQL-database and it should be checked for "Methanol". The Lambda that is build inside is:
{x => (x.Name == "Methanol")}. Now I want to test DateTime fields of the database, but only with the Date-portion. So my Lambda would be
{x => DbFunctions.TruncaateTime(x.EntryDate) == DbFunction.TruncateTime(testDate)}. This works as is, but how can I convert this to an Expression?
Thanks, Hucky
string propertyName = "EntryDate";
DateTime testDate = DateTime.Now;
ParameterExpression parameter = Expression.Parameter(typeof(Chemical), "x");
MemberExpression me = Expression.Property(parameter, propertyName);
var ce = Expression.Convert(me, typeof(DateTime?));
MethodCallExpression mc = Expression.Call(null, typeof(DbFunctions).GetMethod("TruncateTime", new Type[] { typeof(DateTime?) }), ce);
ConstantExpression constant = Expression.Constant(testDate, typeof(DateTime?));
BinaryExpression body = Expression.Equal(mc, constant);
var isEqualExpressionTree = Expression.Lambda<Func<Chemical, bool>>(body, new[] { parameter });
Expression<Func<Chemical, bool>> funcExpression = (Expression<Func<Chemical, bool>>)isEqualExpressionTree;
I have an api Get method which accepts sort property as a string.
What is a best solution to handle it without using switch statement?
public ActionResult Index(string sortOrder)
{
var students = from s in db.Students
select s;
switch (sortOrder)
{
case "LastName":
students = students.OrderByDescending(s => s.LastName);
break;
case "EnrollmentDate":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "Birthday":
students = students.OrderBy(s => s.Birthday);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(students.ToList());
}
Following code I found on microsoft's page, but I believe there is should be more elegant way to do it.
Use the following method to achieve flexible sorting. The method requires property name and sorting direction parameters.
public Func<IQueryable<T>, IOrderedQueryable<T>> GetOrderByExpression<T>(string propertyName, bool isDescending = false)
{
Type typeQueryable = typeof(IQueryable<T>);
ParameterExpression argQueryable = System.Linq.Expressions.Expression.Parameter(typeQueryable, "p");
var outerExpression = System.Linq.Expressions.Expression.Lambda(argQueryable, argQueryable);
var entityType = typeof(T);
ParameterExpression arg = System.Linq.Expressions.Expression.Parameter(entityType, "x");
Expression expression = arg;
PropertyInfo propertyInfo = entityType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
expression = System.Linq.Expressions.Expression.Property(expression, propertyInfo);
LambdaExpression lambda = System.Linq.Expressions.Expression.Lambda(expression, arg);
string methodName = isDescending ? "OrderByDescending" : "OrderBy";
MethodCallExpression resultExp = System.Linq.Expressions.Expression.Call(typeof(Queryable),
methodName,
new Type[] { typeof(T), entityType },
outerExpression.Body,
System.Linq.Expressions.Expression.Quote(lambda));
var finalLambda = System.Linq.Expressions.Expression.Lambda(resultExp, argQueryable);
return (Func<IQueryable<T>, IOrderedQueryable<T>>)finalLambda.Compile();
}
Usage:
IQueryable<Student> query = db.Set<Student>();
var orderBy = GetOrderByExpression<Student>(sortOrder, true);
if(orderBy != null){
query = orderBy(query);
}
I have an extension method for ordering, the sortExpression can be something like "Description" or "Description DESC", it is working perfectly for columns at the same table:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string sortExpression)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(sortExpression))
return source;
var parts = sortExpression.Split(' ');
var isDescending = false;
var propertyName = "";
var type = typeof(T);
if (parts.Length > 0 && parts[0] != "")
{
propertyName = parts[0];
if (parts.Length > 1)
isDescending = parts[1].ToLower().Contains("esc");
var prop = type.GetProperty(propertyName);
if (prop == null)
throw new ArgumentException(string.Format("No property '{0}' on type '{1}'", propertyName, type.Name));
var funcType = typeof(Func<,>)
.MakeGenericType(type, prop.PropertyType);
var lambdaBuilder = typeof(Expression)
.GetMethods()
.First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2)
.MakeGenericMethod(funcType);
var parameter = Expression.Parameter(type);
var propExpress = Expression.Property(parameter, prop);
var sortLambda = lambdaBuilder
.Invoke(null, new object[] { propExpress, new ParameterExpression[] { parameter } });
var sorter = typeof(Queryable)
.GetMethods()
.FirstOrDefault(x => x.Name == (isDescending ? "OrderByDescending" : "OrderBy") && x.GetParameters().Length == 2)
.MakeGenericMethod(new[] { type, prop.PropertyType });
var result = (IQueryable<T>)sorter
.Invoke(null, new object[] { source, sortLambda });
return result;
}
return source;
}
Working Example:
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("Description");
Please note that the "Description" column exists at the same table "Audit".
What I'm trying to do is to sort by a column in a relation table:
Like the following
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("AccessLevel.Name");
Which is equivalent to:
query = query.OrderBy(o => o.AccessLevel.Name);
What is the required modification on my extension method ?
I solved it by using the following code:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, parts[0], "OrderBy");
}
public static IQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(property))
return source;
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { source, lambda });
return (IQueryable<T>)result;
}
In EF, Order By allows a column name to be used to eg OrderBy("Description")
I need to be able to do some thing similar with GroupBy
Other posts have solutions when the column type is known
var groupByExpressionGN2 = GetGroupByExpressionGuidNull<DebtWaiver>("PersonUID");
...
// in the query
.GroupBy(groupByExpression2)
// the Expression function
private static Expression<Func<TEntity,Guid?>> GetGroupByExpressionGuidNull<TEntity>(string property)
{
var item = Expression.Parameter(typeof(TEntity), "gb");
var itemProperty = Expression.PropertyOrField(item, property);
var lambda = Expression.Lambda<Func<TEntity, Guid?>>(itemProperty, item);
return lambda;
}
But my users may select 1 of any columns by which to group by
So how can I make the function above return an expression for group by
I have tried this:
public static Expression<Func<T, object>> GetMember<T>(string memberName)// where T : EntityObject
{
ParameterExpression pe = Expression.Parameter(typeof(T), "p");
System.Reflection.PropertyInfo pi = typeof(T).GetProperty(memberName);
return (Expression<Func<T, object>>)Expression.Lambda<Func<T, object>>(Expression.Convert(Expression.Property(pe, pi), typeof(object)), pe);
}
but it produces : p =>Convert(p.PersonUID)
instead of:p =>p.PersonUID
Regards
GregJF
After a bit more testing (and a good night's sleep) I got the second method to work (Thanks to JA Rreyes )
My issue was that I was using this:
var groupByExpressionGN2 = GetGroupByExpressionGuidNull<DebtWaiver>("PersonUID");
...
// in the query
.GroupBy(groupByExpression2)
I should have being doing this:
var groupByExpression2 = GetMember<DebtWaiver>("PersonUID");
...
// in the query
.GroupBy(groupByExpression2.Compile())
You can use the second method in my original post( GetMember ), but I use this method('cos I like it!): Thanks: Taher Rahgooy
public static Expression<Func<T, object>> GetPropertySelector<T>(string propertyName)
{
var arg = Expression.Parameter(typeof(T), "gb");
var property = Expression.Property(arg, propertyName);
var conv = Expression.Convert(property, typeof(object));
var exp = Expression.Lambda<Func<T, object>>(conv, new ParameterExpression[] { arg });
return exp;
}
Regards
GregJF
I'm creating a dynamic expression builder and trying to implement the 'like' function. Before writing my own I've searched for any existing function and found one close to my need. After several experiments I couldn't bring it to run for types other than string.
When I pass a parameter of type int then I get this error:
Method 'System.String ToString()' declared on type 'System.String' cannot be called with instance of type 'System.Int32'
My code looks like this:
private static MethodCallExpression GetLowerCasePropertyAccess(MemberExpression propertyAccess)
{
//return Expression.Call(Expression.Call(propertyAccess, "ToString", new Type[0]), typeof(string).GetMethod("ToLower", new Type[0]));
return Expression.Call(Expression.Call(propertyAccess, typeof(string).GetMethod("ToString", System.Type.EmptyTypes)), typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
}
private static readonly MethodInfo ContainsMethod = typeof(String).GetMethod("Contains", new Type[] { typeof(String) });
public static Expression<Func<T, bool>> Create<T>(string propertyName, ComparisonOperators comparisonOperator, dynamic comparedValue1, dynamic comparedValue2 = null)
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "x");
MemberExpression memberExpression = Expression.MakeMemberAccess(parameterExpression, typeof(T).GetProperty(propertyName));
ConstantExpression constantExpression = Expression.Constant(comparedValue1, comparedValue1.GetType());
Expression expressionBody = null;
switch (comparisonOperator)
{
...
case ComparisonOperators.Contains:
//var indexOf = Expression.Call(memberExpression, "IndexOf", null, Expression.Constant(comparedValue1, typeof(string)), Expression.Constant(StringComparison.InvariantCultureIgnoreCase));
//expressionBody = Expression.GreaterThanOrEqual(indexOf, Expression.Constant(0));
expressionBody = Expression.Call(GetLowerCasePropertyAccess(memberExpression), ContainsMethod, Expression.Constant(comparedValue1.ToLower()));
break;
}
return Expression.Lambda<Func<T, bool>>(expressionBody, new ParameterExpression[] { parameterExpression });
}
I'm not sure I fully understand what you're doing, but I think your error is caused by this line:
return Expression.Call(Expression.Call(propertyAccess, typeof(string).GetMethod("ToString", System.Type.EmptyTypes)), typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
Which will always try and call the ToString method for a string type, so if you try and use an Int32 property, you're then trying to call String.ToString(), since the implementation of ToString() will be different for different types and the two implementations will not necessarily be compatible, you'll get the exception you're seeing:
Method 'System.String ToString()' declared on type 'System.String' cannot be called with instance of type 'System.Int32'
From what it looks like you're doing, I think this may be what you're after:
return Expression.Call(Expression.Call(propertyAccess, propertyAccess.Type.GetMethod("ToString", System.Type.EmptyTypes)), typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
Which will use the correct implementation of ToString (with type obtained from propertyAccess.Type).
Linq to entities doesn't support .ToString method. For converting numeric values to string you need to use SqlFunctions.StringConvert method. I've fixed your code and now you can do like on string and numeric columns:
private static Expression GetConvertToStringExpression(Expression e)
{
// if property string - no cast needed
// else - use SqlFunction.StringConvert(double?) or SqlFunction.StringConvert(decimal?);
Expression strExpression = null;
if (e.Type == typeof(string))
strExpression = e;
var systemType = Nullable.GetUnderlyingType(e.Type) ?? e.Type;
if (systemType == typeof(int)
|| systemType == typeof(long)
|| systemType == typeof(double)
|| systemType == typeof(short)
|| systemType == typeof(byte)) // continue
{
// cast int to double
var doubleExpr = Expression.Convert(e, typeof (double?));
strExpression = Expression.Call(StringConvertMethodDouble, doubleExpr);
}
if (systemType == typeof (decimal))
{
// call decimal version of StringConvert method
// cast to nullable decimal
var decimalExpr = Expression.Convert(e, typeof (decimal?));
strExpression = Expression.Call(StringConvertMethodDecimal, decimalExpr);
}
return strExpression;
}
private static MethodCallExpression GetLowerCasePropertyAccess(Expression propertyAccess)
{
var stringExpression = GetConvertToStringExpression(propertyAccess);
if (stringExpression == null)
throw new Exception(string.Format("Not supported property type {0}", propertyAccess.Type));
return Expression.Call(stringExpression,
typeof (string).GetMethod("ToLower", Type.EmptyTypes));
}
private static readonly MethodInfo StringConvertMethodDouble = typeof (SqlFunctions).GetMethod("StringConvert",
new Type[] {typeof (double?)});
private static readonly MethodInfo StringConvertMethodDecimal = typeof(SqlFunctions).GetMethod("StringConvert",
new Type[] { typeof(decimal?) });
I have just made something like that:
public Expression<Func<T,bool>> BuildContainsExpression<T>(MemberExpression memberExp, object comparedValue)
{
var parameter = Expression.Parameter(memberExp.Member.DeclaringType, "x");
var method = typeof(string).GetMethod("Contains", types: new[] { typeof(string) });
var comparison = Expression.Equal(
Expression.Call(
method: method,
instance: memberExp,
arguments: Expression.Constant(comparedValue)),
Expression.Constant(true)
);
return Expression.Lambda<Func<T, bool>>(comparison, parameter);
}
And it builds an expression like below:
x.Language.Contains("tr")
(with my dynamic parameters)