Reputation: 4254
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;
}
}
Upvotes: 1
Views: 1000
Reputation: 205539
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
);
Upvotes: 2