Skip to content

Instantly share code, notes, and snippets.

@sviataslau
Last active December 11, 2015 20:09
Show Gist options
  • Save sviataslau/4653382 to your computer and use it in GitHub Desktop.
Save sviataslau/4653382 to your computer and use it in GitHub Desktop.
Often you need to display and sort data loaded from database using Entity Framework. In general the displayed data is a set of model/viewmodel objects that are not directly mapped to EF model. Such a trivial task! But in reality it is not so trivial if you're using EF. So this gist is about a way to provide generic sorting mechanism for such sit…
namespace Sorting
{
public static class Expression
{
public static Type GetExpressionReturningType(this LambdaExpression expression)
{
MemberInfo memberInfo = GetMemberInfo(expression);
if (memberInfo != null)
return GetMemberUnderlyingType(memberInfo);
MethodInfo methodInfo = GetMethodInfo(expression);
if (methodInfo != null)
return methodInfo.ReturnType;
throw new NotImplementedException(string.Format("Can't get type for passed expression {0}", expression));
}
private static MemberInfo GetMemberInfo(this LambdaExpression expression)
{
MemberExpression memberExpression = expression.Body as MemberExpression;
if (memberExpression != null)
return memberExpression.Member;
UnaryExpression unaryExpression = expression.Body as UnaryExpression;
if (unaryExpression == null)
return null;
return ((MemberExpression)unaryExpression.Operand).Member;
}
private static MethodInfo GetMethodInfo(this LambdaExpression expression)
{
return expression.Compile().Method;
}
private static Type GetMemberUnderlyingType(MemberInfo member)
{
switch (member.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo)member).FieldType;
case MemberTypes.Property:
return ((PropertyInfo)member).PropertyType;
case MemberTypes.Event:
return ((EventInfo)member).EventHandlerType;
default:
throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", "member");
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Expression = System.Linq.Expressions.Expression;
namespace Sorting
{
public class QueryableSorterFactory
{
public static QueryableSorter<T> CreateSorter<T>(IQueryable<T> query)
{
return new QueryableSorter<T>(query);
}
}
public class QueryableSorter<T>
{
private class QueryableSortClause
{
public QueryableSortClause(LambdaExpression sortExpression, Type type)
{
SortExpression = sortExpression;
SortExpressionResultType = type;
}
public QueryableSortClause(LambdaExpression sortExpression)
: this(sortExpression, sortExpression.GetExpressionReturningType()) { }
public LambdaExpression SortExpression { get; private set; }
public Type SortExpressionResultType { get; private set; }
}
private readonly IQueryable<T> _query;
private readonly IDictionary<string, IEnumerable<LambdaExpression>> _sortDescriptorMappings = new Dictionary<string, IEnumerable<LambdaExpression>>();
internal QueryableSorter(IQueryable<T> query)
{
_query = query;
}
public void AddSortingRoute<TSortExpression1>(string descriptor, Expression<Func<T, TSortExpression1>> firstSortExpression)
{
_sortDescriptorMappings.Add(descriptor, new[] { firstSortExpression });
}
public void AddSortingRoute<TSortExpression1, TSortExpression2>(string descriptor, Expression<Func<T, TSortExpression1>> firstSortExpression, Expression<Func<T, TSortExpression2>> secondSortExpression = null)
{
var addedExpressions = new LambdaExpression[] { firstSortExpression, secondSortExpression };
_sortDescriptorMappings.Add(descriptor, addedExpressions.Where(e => e != null));
}
public void AddSortingRoute<TSortExpression1, TSortExpression2, TSortExpression3>(string descriptor, Expression<Func<T, TSortExpression1>> firstSortExpression, Expression<Func<T, TSortExpression2>> secondSortExpression = null, Expression<Func<T, TSortExpression3>> thirdSortExpression = null)
{
var addedExpressions = new LambdaExpression[] { firstSortExpression, secondSortExpression, thirdSortExpression };
_sortDescriptorMappings.Add(descriptor, addedExpressions.Where(e => e != null));
}
public IQueryable<T> Sort(IEnumerable<SortCriteria> criteria)
{
IQueryable<T> result = _query;
if (criteria != null && criteria.Any())
{
var criterias = criteria.ToArray();
for (int i = 0; i < criterias.Count(); i++)
{
SortCriteria sd = criterias[i];
result = OrderBy(result, sd.PropertyName, sd.IsDescending, i != 0);
}
}
return result;
}
private IOrderedQueryable<T> OrderBy(IQueryable<T> source, string descriptor, bool descending, bool thenBy)
{
QueryableSortClause[] sortClauses = GetSortClauses(descriptor).ToArray();
object result = source;
for (int i = 0; i < sortClauses.Count(); i++)
{
QueryableSortClause clause = sortClauses[i];
var methodName = GetSortMethodName(descending, thenBy || i != 0);
result = CreateSortMethod(methodName, clause.SortExpressionResultType).Invoke(null, new[] { result, clause.SortExpression });
}
return (IOrderedQueryable<T>)result;
}
private static MethodInfo CreateSortMethod(string methodName, Type sortPropertyType)
{
return typeof(Queryable).GetMethods()
.Single(method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2).MakeGenericMethod(typeof(T), sortPropertyType);
}
private static string GetSortMethodName(bool descending, bool thenBy)
{
string methodName;
if (!thenBy)
methodName = descending ? "OrderByDescending" : "OrderBy";
else
methodName = descending ? "ThenByDescending" : "ThenBy";
return methodName;
}
private IEnumerable<QueryableSortClause> GetSortClauses(string descriptor)
{
Type sortPropertyType;
return CreateSortClauseWithDescriptorMapping(descriptor) ?? new[] { new QueryableSortClause(CreateDefaultSortClause(descriptor, out sortPropertyType), sortPropertyType) };
}
private static LambdaExpression CreateDefaultSortClause(string descriptor, out Type sortPropertyType)
{
var type = typeof(T);
string[] properties = descriptor.Split('.');
ParameterExpression parameter = Expression.Parameter(type, "p");
Expression expr = parameter;
bool sortPropertyWasFound = false;
foreach (string prop in properties)
{
PropertyInfo pi = type.GetProperty(prop);
if (pi == null)
continue;
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
sortPropertyWasFound = true;
}
if (!sortPropertyWasFound)
throw new InvalidOperationException("Sort property wasn't found.");
sortPropertyType = type;
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
return Expression.Lambda(delegateType, expr, parameter);
}
private IEnumerable<QueryableSortClause> CreateSortClauseWithDescriptorMapping(string descriptor)
{
if (!_sortDescriptorMappings.ContainsKey(descriptor))
return null;
IEnumerable<LambdaExpression> objectProperty = _sortDescriptorMappings[descriptor];
return objectProperty.Select(e => new QueryableSortClause(e));
}
}
}
using Sorting;
namespace Sorting.Test
{
[TestClass]
public class QueryableSorterTest
{
[TestMethod]
public void should_sort()
{
using (var context = new ObjectContext())
{
var query = (from e in context.ENTITY
join re in context.RELATED_ENTITY on e.ENTITY_ID equals re.ENTITY_ID
join se in context.SUBENTITY on e.ENTITY_ID equals se.ENTITY_ID
select new
{
e.ID,
e.NAME,
e.NUMBER,
re.IS_VALID
});
var sorter = QueryableSorterFactory.CreateSorter(query);
sorter.AddSortingRoute("Entity Id", e => e.ID);
sorter.AddSortingRoute("Name / Number", e => e.NAME, e => e.NUMBER);
sorter.AddSortingRoute("Is Valid", e => e.IS_VALID);
var sorted = sorter.Sort(new[] { new SortCriteria("Is Valid", true), new SortCriteria("Name / Number", false), new SortCriteria("Entity Id", true) });
Assert.IsNotNull(sorted);
}
}
}
}
using System.Runtime.Serialization;
namespace Sorting
{
[DataContract]
public class SortCriteria
{
public SortCriteria(string propertyName, bool isDescending)
{
PropertyName = propertyName;
IsDescending = isDescending;
}
[DataMember]
public string PropertyName { get; private set; }
[DataMember]
public bool IsDescending { get; private set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment