21

I used the following methods to construct Order By Expression. Original Source

It is really slick. The downside is it only works if Property is string type.

How can I make it to accept different Property type without creating a bunch of methods for different data types?

public static bool PropertyExists<T>(string propertyName)
{
    return typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase |
      BindingFlags.Public | BindingFlags.Instance) != null;
}

public static Expression<Func<T, string>> GetPropertyExpression<T>(string propertyName)
{
    if (typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
        BindingFlags.Public | BindingFlags.Instance) == null)
    {
        return null;
    }

    var paramterExpression = Expression.Parameter(typeof(T));

    return (Expression<Func<T, string>>)Expression.Lambda(
        Expression.PropertyOrField(paramterExpression, propertyName), paramterExpression);
}

Usage

// orderBy can be either Name or City.
if (QueryHelper.PropertyExists<Club>(orderBy)) 
{ 
   var orderByExpression = QueryHelper.GetPropertyExpression<Club>(orderBy); 
   clubQuery = clubQuery.OrderBy(orderByExpression); 
} 
else 
{ 
   clubQuery = clubQuery.OrderBy(c => c.Id); 
} 

Problem

public class Club 
{ 
  public int Id { get; set; } 
  public string Name { get; set; } 
  public string City { get; set; } 
  public DateTime CreateDate { get; set; } <= this won't work
} 

My Current Approach (Too many if statements)

public static Expression<Func<TSource, TKey>> 
    GetPropertyExpression<TSource, TKey>(string propertyName)
{
    if (typeof (TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | 
        BindingFlags.Public | BindingFlags.Instance) == null)
    {
        return null;
    }
    var paramterExpression = Expression.Parameter(typeof (TSource));
    return (Expression<Func<TSource, TKey>>) 
        Expression.Lambda(Expression.PropertyOrField(
           paramterExpression, propertyName), paramterExpression);
}

The downside is I end up with a lot of if statements for each datatype.

if (QueryHelper.PropertyExists<Club>(orderBy)) 
{
   if(orderBy == "CreateDate")
   {       
      var orderByExpression = GetPropertyExpression<Club, DateTime>(orderBy);
      ...
   }
   else if(orderBy == "Name" || orderBy == "City")
   {
      var orderByExpression = GetPropertyExpression<Club, string>(orderBy);
      ...
   }
   ...
}
else 
{ 
   clubQuery = clubQuery.OrderBy(c => c.Id); 
} 
Win
  • 56,078
  • 13
  • 92
  • 167
  • So it is at the `var orderByExpression = QueryHelper.GetPropertyExpression(orderBy);` that it causes the problem? – krillgar Jan 20 '16 at 17:38
  • The problem is too many repeated code for each data type. Please kindly refer to the last code I posted. It seems that very common problem for whoever use dynamic **OrderBy**; but I could not find any solution in Google. ***I'm open to any suggestion or alternative approach***. – Win Jan 20 '16 at 17:47
  • Rather than passing in "CreateDate" or "Name" or "City" or what not, I'd pass in some sort of object that represents the property to sort by. This object would have the ability to generate a sort expression, and other useful things. More code, but no messy *if* jungle. – Mike Christensen Jan 20 '16 at 18:04
  • @MikeChristensen what would be the type of the returned `Expression`? – moarboilerplate Jan 20 '16 at 18:27
  • @moarboilerplate - Hmm, I suppose `LambdaExpression`? The only thing `Expression` gives you is `Compile` and `Update` which he probably doesn't need. – Mike Christensen Jan 20 '16 at 18:37
  • @MikeChristensen the difficulty is actually with `TDelegate`. In order to return a delegate that can handle different types, the delegate signature has to use an intrinsically generic type like `object`, and then after you get your object, you have to figure out how to cast it. – moarboilerplate Jan 20 '16 at 18:40
  • Yea, I'm saying the implementations for each field type would do that.. There should be a `CreateDateField` class, and `CityField` class, etc. Each would implement an interface, which has a `GenerateSortExpression` that returns `LambaExpression` – Mike Christensen Jan 20 '16 at 18:44

2 Answers2

26

I found a solution with the help of Jon Skeet's old answer.

public static class QueryHelper
{
    private static readonly MethodInfo OrderByMethod =
        typeof (Queryable).GetMethods().Single(method => 
        method.Name == "OrderBy" && method.GetParameters().Length == 2);

    private static readonly MethodInfo OrderByDescendingMethod =
        typeof (Queryable).GetMethods().Single(method => 
        method.Name == "OrderByDescending" && method.GetParameters().Length == 2);

    public static bool PropertyExists<T>(this IQueryable<T> source, string propertyName)
    {
        return typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase |
            BindingFlags.Public | BindingFlags.Instance) != null;
    }

    public static IQueryable<T> OrderByProperty<T>(
       this IQueryable<T> source, string propertyName)
    {
        if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
            BindingFlags.Public | BindingFlags.Instance) == null)
        {
            return null;
        }
        ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
        Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
        LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
        MethodInfo genericMethod = 
          OrderByMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
        object ret = genericMethod.Invoke(null, new object[] {source, lambda});
        return (IQueryable<T>) ret;
    }

    public static IQueryable<T> OrderByPropertyDescending<T>(
        this IQueryable<T> source, string propertyName)
    {
        if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
            BindingFlags.Public | BindingFlags.Instance) == null)
        {
            return null;
        }
        ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
        Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
        LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
        MethodInfo genericMethod = 
          OrderByDescendingMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
        object ret = genericMethod.Invoke(null, new object[] {source, lambda});
        return (IQueryable<T>) ret;
    }
}

Usage

string orderBy = "Name";
if (query.PropertyExists(orderBy))
{
   query = query.OrderByProperty(orderBy);
   - OR - 
   query = query.OrderByPropertyDescending(orderBy);
}
kumaheiyama
  • 594
  • 1
  • 12
  • 27
Win
  • 56,078
  • 13
  • 92
  • 167
  • 5
    Did we really expect anyone else to have an answer? ;) – krillgar Jan 20 '16 at 18:51
  • Is it possible to modify this to also sort on Navigation properties of current entity ? – SJMan Apr 18 '19 at 10:35
  • Excellent solution! My only question is how can I order by an subordinate object? For instance, if I have an object `Apple { Kernel { Size = 13 } }` how would I order the query on the `Size` property? Setting `orderBy` in the example to `"Kernel.Size"` doesn't work. – kumaheiyama Nov 13 '19 at 16:18
  • This doesn't work with EF Core 3.1. Any suggestions on how to make it work right? – James Hancock Jan 30 '20 at 15:45
0

I have, IMHO, a simpler solution:

public static IOrderedQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source, bool ascending , string sortingProperty)
{
    if (ascending)
        return source.OrderBy(item => item.GetReflectedPropertyValue(sortingProperty));
    else
        return source.OrderByDescending(item => item.GetReflectedPropertyValue(sortingProperty));
}

private static object GetReflectedPropertyValue(this object subject, string field)
{
    return subject.GetType().GetProperty(field).GetValue(subject, null);
}

The usage is:

myQueryableCollection.Sort(ascending: true, "Name")

Of course, the PropertyExist() helper is a great addition...

Romias
  • 12,977
  • 7
  • 51
  • 79
  • 1
    Hi, does this work in EF Core 3? It throws for me: .OrderByDescending(e0 => (object)e0 .GetReflectedPropertyValue(__filter_Sort_Predicate_3))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToLi st(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. – Tomas Bruckner Jan 28 '20 at 12:01
  • Doesn't work: `The LINQ expression 'DbSet .Where(i => .....) .OrderBy(i => (object)i .GetReflectedPropertyValue(__sortingProperty_0))' could not be translated. ` – Ian Dec 18 '20 at 15:42