1

I have a method that orders a queryable by a property via reflection, like so:

   public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByProperty, bool desc)
   {
       string command = desc ? "OrderByDescending" : "OrderBy";
       var parts = orderByProperty.Split('.');
       var parameter = Expression.Parameter(typeof(TEntity), "p");
       Expression body = parameter;
       if (parts.Length > 1)
       {
           for (var i = 0; i < parts.Length - 1; i++)
           {
               body = Expression.Property(body, parts[i]);
           }
       }
       var property = body.Type.GetProperty(parts.LastOrDefault())
       body = Expression.Property(body, property.Name);
       var orderByExpression = Expression.Lambda(body, parameter);
       resultExpression = Expression.Call(typeof(Queryable), command, 
                                       new Type[] { typeof(TEntity), property.PropertyType },
                                       source.Expression, Expression.Quote(orderByExpression));
       return source.Provider.CreateQuery<TEntity>(resultExpression);
   }

And it works if I use it for the object's properties or its children's properties, but it doesn't work when a child property is a List.

    // Success
    db.Users.OrderBy("Name", true)
    db.Users.OrderBy("CurrentAddress.City.Name", false)

    // Fail
    db.Users.OrderBy("PhoneLines.RenewalDate", true)

My model is like so:

    public class User {
        public string Name { get; set; }
        public Address CurrentAddress{ get; set; }
        public List<Phone> PhoneLines { get; set; }
    }

And I am trying to do something like this work:

    db.Users.OrderByDescending(x => 
        x.PhoneLines
          .OrderByDescending(y => y.RenewalDate)
          .Select(y => y.RenewalDate).FirstOrDefault()
    );

Right now I am only interested in making this work for first level List children, that is, I am not concerned about a cascading list scenario, just one child in the chain is a list.

So far my attempts have failed. I am checking if the last property is a collection and if so i tried creating a lambda for the list and another lambda for the class' element target order property.


if (body.Type.Namespace.Equals("System.Collections.Generic"))
{
 var mainType = body.Type.GenericTypeArguments.FirstOrDefault();
 var parameterInner = Expression.Parameter(mainType, "x");
 var mainProperty = Expression.Property(parameterInner, property.Name);
 var orderByExpressionInner = Expression.Lambda(mainProperty, parameterInner);
 var orderByExpression = Expression.Lambda(body, parameter);

 var selM = typeof(Enumerable).GetMethods()
             .Where(x => x.Name == command)
             .First().MakeGenericMethod(mainType, property.PropertyType);

 var resultExpressionOuter = Expression.Call(typeof(Queryable), 
                            command, new Type[] { typeof(TEntity), body.Type },
                            source.Expression, 
                            Expression.Call(null, selM, parameterInner, orderByExpressionInner
                        ));
}

My knowledge with reflection is limited, is it possible to achieve this via reflection?

  • `So far my attempts have failed` can you elaborate on that? What happens and what do you expect instead? – thehennyy May 22 '19 at 10:55
  • The latest attempt results in the following exception: `Expression of type 'Phone' cannot be used for parameter of type 'System.Collections.Generic.IEnumerable'1[Phone]' of method 'System.Linq.IOrderedEnumerable'1[Phone] OrderBy[Phone,System.DateTime](System.Collections.Generic.IEnumerable'1[Phone], System.Func'2[Phone,System.DateTime])'` I expect it to build an expression successfully orders an EF dataset with `source.Provider.CreateQuery(resultExpression)` – Bruno Di Giuseppe May 22 '19 at 13:01

1 Answers1

1

One option is to place a new property on your parent class and make this responsible for returning the value which you require. Such as this;

public class User
{
    public string Name { get; set; }
    public Address CurrentAddress { get; set; }
    public List<Phone> PhoneLines { get; set; }

    private DateTime _dummyValue;
    public DateTime GetRenewalDate
    {
        get
        {
            if (this.PhoneLines == null || this.PhoneLines.Count == 0)
                return DateTime.MinValue;
            else
            {
                return this.PhoneLines.OrderByDescending(y => y.RenewalDate).Select(y => y.RenewalDate).FirstOrDefault();
            }
        }
        set
        {
             _dummyValue = value;
        }
    }
 }

Make sure you have default handling in case your list is empty or null, here I return MinValue of DateTime.

Edit: To get around the other issue you have put in the comments

The specified type member 'GetRenewalDate' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

You could try adding a dummy set, this might also work as a workaround (but is not elegant). I have edited the code above to show this

jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
  • That is a clever solution, but it it's throwing `'The specified type member 'GetRenewalDate' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'` – Bruno Di Giuseppe May 22 '19 at 13:21
  • Oh, ok! A "dirty" solution is to convert to a list. See the 2nd answer here: https://stackoverflow.com/questions/11584660/the-specified-type-member-is-not-supported-in-linq-to-entities-only-initializer – jason.kaisersmith May 22 '19 at 13:26
  • @BrunoDiGiuseppe I have updated the solution to use a dummy set, this sometimes works but is very dirty! – jason.kaisersmith May 23 '19 at 05:18