Ho un'estensione nel mio progetto che mi consente di ordinare un oggetto IEnumerable
con una stringa, in modo che l'ordinamento possa essere eseguito in modo più dinamico.
Quindi, se ho questi modelli:
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;}
}
Potrei ordinare la mia lista in questo modo:
var myList = db.MyModel.Where(m => m.IsActive);
myList
.OrderBy(m => m.MyChildren
.OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate);
o:
var myList = db.MyModel.Where(m => m.IsActive);
myList.OrderBy(m => m.MyChildren.Max(c => c.SavedDate);
Ma voglio essere in grado di ordinare dinamicamente, in base alle opzioni dell'utente. Quindi vorrei questo:
var myList = db.MyModel.Where(m => m.IsActive);
myList.OrderByField("MyChildren.SavedDate");
Il metodo di estensione che ho finora assomiglia a questo:
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;
}
}
Fondamentalmente dovresti usare il seguente overload di Expression.Call
che ti permette di costruire un'espressione per chiamare metodi generici statici (quali sono tutti i metodi di estensione LINQ).
Per costruire l'equivalente di espressioni come questa
m => m.MyChildren.OrderByDescending(c => c.SavedDate).FirstOrDefault().SavedDate
puoi usare il seguente 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);
Ma nota che otterrai NRE se la raccolta è vuota.
Quindi è meglio che tu costruisca un'espressione come questa:
m => m.MyChildren.OrderByDescending(c => c.SavedDate)
.Select(c => (DateTime?)c.SavedDate).FirstOrDefault()
con:
// 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
);