Last active
December 11, 2015 20:09
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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