1

I have an extension in my project that let's me sort an IEnumerable with a string, so that sorting can be done more dynamically.

So, if I have these models:

public MyModel
{
    public int Id {get; set;}
    public string RecordName {get; set;}
    public ChildModel MyChildren {get; set;}
}

public ChildModel 
{
    public int ChildModelId {get; set;}
    public string ChildName {get; set;}
    public DateTime SavedDate {get; set;}
}

I could sort my list like this:

var myList = db.MyModel.Where(m => m.IsActive);
myList
    .OrderBy(m => m.MyChildren
        .OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate);

or:

var myList = db.MyModel.Where(m => m.IsActive);
myList.OrderBy(m => m.MyChildren.Max(c => c.SavedDate);

But I want to be able to dynamically sort, based on user options. So I would like this:

var myList = db.MyModel.Where(m => m.IsActive);
myList.OrderByField("MyChildren.SavedDate");

The extension method I have so far looks like this:

public static class MkpExtensions
{
    public static IEnumerable<T> OrderByField<T>(this IEnumerable<T> list, string sortExpression)
    {
        sortExpression += "";
        string[] parts = sortExpression.Split(' ');
        bool descending = false;
        string fullProperty = "";

        if (parts.Length > 0 && parts[0] != "")
        {
            fullProperty = parts[0];

            if (parts.Length > 1)
            {
                descending = parts[1].ToLower().Contains("esc");
            }

            ParameterExpression inputParameter = Expression.Parameter(typeof(T), "p");
            Expression propertyGetter = inputParameter;

            foreach (string propertyPart in fullProperty.Split('.'))
            {
                var checkIfCollection = propertyGetter.Type.GetInterfaces()//(typeof (ICollection<>).FullName);
                    .Any(x => x.IsGenericType &&
                        (x.GetGenericTypeDefinition() == typeof(ICollection<>) || x.GetGenericTypeDefinition() == typeof(IEnumerable<>)));

                if (checkIfCollection)
                {
                    var pgType = propertyGetter.Type;
                    var childType = pgType.GetGenericArguments().Single();
                    var childProp = childType.GetProperty(propertyPart);

                    ParameterExpression childInParam = Expression.Parameter(childType, "c");
                    var propertyAccess = Expression.Property(childInParam, childProp);                      
                    var orderByExp = Expression.Lambda(propertyAccess, childInParam);
                    // At this point, orderByExp is c => c.ActionDate

                    // Now I want to build the expression tree to handle the order by
                    XXXXX This is where I need help.
                }
                else
                {
                    // This handles a singular property. Like "MyChildren.ChildName"
                    // and this part does work
                    PropertyInfo prop = propertyGetter.Type.GetProperty(propertyPart);
                    if (prop == null)
                        throw new Exception("No property '" + fullProperty + "' in + " + propertyGetter.Type.Name + "'");
                    propertyGetter = Expression.Property(propertyGetter, prop);
                }
            }

            Expression conversion = Expression.Convert(propertyGetter, typeof(object));
            var getter = Expression.Lambda<Func<T, object>>(conversion, inputParameter).Compile();

            if (descending)
            {
                // This would be like 
                //  list.OrderByDescending(m => m.MyChildren
                //      .OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate);
                return list.OrderByDescending(getter);
            }
            else
            {
                // This would be like 
                //  list.OrderBy(m => m.MyChildren
                //      .OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate);
                return list.OrderBy(getter);
            }
        }

        return list;
    }
}
M Kenyon II
  • 3,724
  • 4
  • 35
  • 83
  • 1
    Possible duplicate of [Dynamic LINQ OrderBy on IEnumerable](http://stackoverflow.com/questions/41244/dynamic-linq-orderby-on-ienumerablet) – ASh May 10 '16 at 17:30
  • @Ash I don't see how that handles sorting on a property of a collection property. – M Kenyon II May 10 '16 at 18:40

1 Answers1

2

Basically you should use the following Expression.Call overload which allows you to build an expression for calling static generic methods (what are all the LINQ extension methods).

To build the equivalent of expression like this

m => m.MyChildren.OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate

you can use the following snippet:

// At this point, orderByExp is c => c.ActionDate
var orderByDescendingCall = Expression.Call(
    typeof(Enumerable), "OrderByDescending", new Type[] { childType, orderByExp.Body.Type },
    propertyGetter, orderByExp
);
var firstOrDefaultCall = Expression.Call(
    typeof(Enumerable), "FirstOrDefault", new Type[] { childType },
    orderByDescendingCall
);
propertyGetter = Expression.Property(firstOrDefaultCall, childProp);

But note that you'll get NRE if the collection is empty.

So you'd better build an expression like this:

m => m.MyChildren.OrderByDescending(c => c.SavedDate)
   .Select(c => (DateTime?)c.SavedDate).FirstOrDefault()

with:

// At this point, orderByExp is c => c.ActionDate
var orderByDescendingCall = Expression.Call(
    typeof(Enumerable), "OrderByDescending", new Type[] { childType, orderByExp.Body.Type },
    propertyGetter, orderByExp
);
Expression propertySelector = propertyAccess;
// If value type property and not nullable, convert it to nullable
if (propertySelector.Type.IsValueType && Nullable.GetUnderlyingType(propertySelector.Type) == null)
    propertySelector = Expression.Convert(propertySelector, typeof(Nullable<>).MakeGenericType(propertySelector.Type));
var selectCall = Expression.Call(
    typeof(Enumerable), "Select", new Type[] { childType, propertySelector.Type },
    orderByDescendingCall, Expression.Lambda(propertySelector, childInParam)
);
propertyGetter = Expression.Call(
    typeof(Enumerable), "FirstOrDefault", new Type[] { propertySelector.Type },
    selectCall
);
Ivan Stoev
  • 159,890
  • 9
  • 211
  • 258
  • I'm using LINQPad to work this out. With your Expression.Call, I'm getting the message 'No generic method 'OrderByDescending' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments.' Am I missing a using clause? `childType` and `orderByExp.Body.Type` are both `DateTime`. `propertyGetter.Type` is `System.Collections.Generic.ICollection'1[PublicationSystem.Model.ActionItem]` – M Kenyon II May 10 '16 at 19:08
  • `childType` should be `ChildModel`. Your sample class is incorrect, `MyChildren` should be `IEnumerable`, or `ICollection` or `List` in order to make your manual expression examples even compile. – Ivan Stoev May 10 '16 at 19:15
  • I was using the wrong variable to get the type. I'm making progress now. – M Kenyon II May 10 '16 at 19:59