Храните строки NULL последним в Dynamic Linq Order By
Я использую этот фрагмент ниже для упорядочивания моих запросов Linq динамически и отлично работает. Я не очень разбираюсь в сложных или сложных запросах linq, но мне нужно, чтобы при использовании возрастающего значения эти значения NULL были последними и наоборот.
Итак, если мое имя свойства было целым числом, а значения столбца были 1, 3, 5, все NULL-строки были бы в конце, а не в начале по умолчанию. Что я могу добавить к этому выражению, чтобы это произошло?
Этот код работает с инфраструктурой сущностей и все еще нуждается в сравнении NULL.
Пример
list.OrderBy("NAME DESC").ToList()
Класс
public static class OrderByHelper
{
public static IOrderedQueryable<T> ThenBy<T>(this IEnumerable<T> enumerable, string orderBy)
{
return enumerable.AsQueryable().ThenBy(orderBy);
}
public static IOrderedQueryable<T> ThenBy<T>(this IQueryable<T> collection, string orderBy)
{
if (string.IsNullOrWhiteSpace(orderBy))
orderBy = "ID DESC";
IOrderedQueryable<T> orderedQueryable = null;
foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, false))
orderedQueryable = ApplyOrderBy<T>(collection, orderByInfo);
return orderedQueryable;
}
public static IOrderedQueryable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy)
{
return enumerable.AsQueryable().OrderBy(orderBy);
}
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy)
{
if (string.IsNullOrWhiteSpace(orderBy))
orderBy = "ID DESC";
IOrderedQueryable<T> orderedQueryable = null;
foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, true))
orderedQueryable = ApplyOrderBy<T>(collection, orderByInfo);
return orderedQueryable;
}
private static IOrderedQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo)
{
string[] props = orderByInfo.PropertyName.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
string methodName = String.Empty;
if (!orderByInfo.Initial && collection is IOrderedQueryable<T>)
{
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "ThenBy";
else
methodName = "ThenByDescending";
}
else
{
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "OrderBy";
else
methodName = "OrderByDescending";
}
return (IOrderedQueryable<T>)typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { collection, lambda });
}
private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy, bool initial)
{
if (String.IsNullOrEmpty(orderBy))
yield break;
string[] items = orderBy.Split(',');
foreach (string item in items)
{
string[] pair = item.Trim().Split(' ');
if (pair.Length > 2)
throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item));
string prop = pair[0].Trim();
if (String.IsNullOrEmpty(prop))
throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");
SortDirection dir = SortDirection.Ascending;
if (pair.Length == 2)
dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);
yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };
initial = false;
}
}
private class OrderByInfo
{
public string PropertyName { get; set; }
public SortDirection Direction { get; set; }
public bool Initial { get; set; }
}
private enum SortDirection
{
Ascending = 0,
Descending = 1
}
Ответы
Ответ 1
Это относительно просто. Для каждого переданного сортировщика сортировки метод выполняет одно из следующих действий:
.OrderBy(x => x.Member)
.ThenBy(x => x.Member)
.OrderByDescending(x => x.Member)
.ThenByDescendiong(x => x.Member)
Когда тип x.Member
является ссылочным типом или типом значения NULL, желаемое поведение может быть достигнуто путем предварительного упорядочения в том же направлении следующим выражением
x => x.Member == null ? 1 : 0
Некоторые люди используют порядок bool
, но я предпочитаю быть явным и использовать условный оператор со специальными целыми значениями. Таким образом, соответствующие вызовы для вышеуказанных вызовов:
.OrderBy(x => x.Member == null ? 1 : 0).ThenBy(x => x.Member)
.ThenBy(x => x.Member == null ? 1 : 0).ThenBy(x => x.Member)
.OrderByDescending(x => x.Member == null ? 1 : 0).ThenByDescending(x => x.Member)
.ThenByDescending(x => x.Member == null ? 1 : 0).ThenByDescending(x => x.Member)
то есть. исходный метод для выражения pre order, за которым следует ThenBy(Descending)
с исходным выражением.
Вот реализация:
public static class OrderByHelper
{
public static IOrderedQueryable<T> ThenBy<T>(this IEnumerable<T> source, string orderBy)
{
return source.AsQueryable().ThenBy(orderBy);
}
public static IOrderedQueryable<T> ThenBy<T>(this IQueryable<T> source, string orderBy)
{
return OrderBy(source, orderBy, false);
}
public static IOrderedQueryable<T> OrderBy<T>(this IEnumerable<T> source, string orderBy)
{
return source.AsQueryable().OrderBy(orderBy);
}
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string orderBy)
{
return OrderBy(source, orderBy, true);
}
private static IOrderedQueryable<T> OrderBy<T>(IQueryable<T> source, string orderBy, bool initial)
{
if (string.IsNullOrWhiteSpace(orderBy))
orderBy = "ID DESC";
var parameter = Expression.Parameter(typeof(T), "x");
var expression = source.Expression;
foreach (var item in ParseOrderBy(orderBy, initial))
{
var order = item.PropertyName.Split('.')
.Aggregate((Expression)parameter, Expression.PropertyOrField);
if (!order.Type.IsValueType || Nullable.GetUnderlyingType(order.Type) != null)
{
var preOrder = Expression.Condition(
Expression.Equal(order, Expression.Constant(null, order.Type)),
Expression.Constant(1), Expression.Constant(0));
expression = CallOrderBy(expression, Expression.Lambda(preOrder, parameter), item.Direction, initial);
initial = false;
}
expression = CallOrderBy(expression, Expression.Lambda(order, parameter), item.Direction, initial);
initial = false;
}
return (IOrderedQueryable<T>)source.Provider.CreateQuery(expression);
}
private static Expression CallOrderBy(Expression source, LambdaExpression selector, SortDirection direction, bool initial)
{
return Expression.Call(
typeof(Queryable), GetMethodName(direction, initial),
new Type[] { selector.Parameters[0].Type, selector.Body.Type },
source, Expression.Quote(selector));
}
private static string GetMethodName(SortDirection direction, bool initial)
{
return direction == SortDirection.Ascending ?
(initial ? "OrderBy" : "ThenBy") :
(initial ? "OrderByDescending" : "ThenByDescending");
}
private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy, bool initial)
{
if (String.IsNullOrEmpty(orderBy))
yield break;
string[] items = orderBy.Split(',');
foreach (string item in items)
{
string[] pair = item.Trim().Split(' ');
if (pair.Length > 2)
throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item));
string prop = pair[0].Trim();
if (String.IsNullOrEmpty(prop))
throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");
SortDirection dir = SortDirection.Ascending;
if (pair.Length == 2)
dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);
yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };
initial = false;
}
}
private class OrderByInfo
{
public string PropertyName { get; set; }
public SortDirection Direction { get; set; }
public bool Initial { get; set; }
}
private enum SortDirection
{
Ascending = 0,
Descending = 1
}
}
Ответ 2
Один из подходов состоит в том, чтобы передать дополнительное выражение для тестирования для null
в этот метод и использовать его в дополнительном предложении OrderBy
/ThenBy
.
Будет создано два предложения OrderBy
- первое будет на nullOrder
, а второе - на фактическое свойство.
private static IOrderedQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo, Expression<Func<T,int>> nullOrder) {
...
if (!orderByInfo.Initial && collection is IOrderedQueryable<T>) {
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "ThenBy";
else
methodName = "ThenByDescending";
} else {
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "OrderBy";
else
methodName = "OrderByDescending";
}
if (nullOrder != null) {
collection = (IQueryable<T>)typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { collection, nullOrder });
// We've inserted the initial order by on nullOrder,
// so OrderBy on the property becomes a "ThenBy"
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "ThenBy";
else
methodName = "ThenByDescending";
}
// The rest of the method remains the same
return (IOrderedQueryable<T>)typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { collection, lambda });
}
Вызывателю нужно будет явно передать нулевой контролер. Передача null
для полей с недействительными значениями должна работать. Вы можете построить их один раз и передать по мере необходимости:
static readonly Expression<Func<string,int>> NullStringOrder = s => s == null ? 1 : 0;
static readonly Expression<Func<int?,int>> NullIntOrder = i => !i.HasValue ? 1 : 0;
static readonly Expression<Func<long?,int>> NullLongOrder = i => !i.HasValue ? 1 : 0;
Ответ 3
Мой подход заключается в создании универсального класса, реализующего IComparer<TClass>
. Таким образом, вы можете использовать свой класс во всех операторах LINQ с нестандартным компаратором. Преимущество заключается в том, что во время компиляции у вас будет полная проверка типов. Вы не можете назвать свойства, которые не могут быть сопоставлены или которые не могут быть null
class NullValueLastComparer<TClass, TKey> : IComparer<TClass>
where TClass : class
where TKey : IComparable<TKey>
{
Этот общий класс имеет два параметра типа: класс, который вы хотите сравнить, и тип свойства, с которым вы хотите сравнить. Предложения where утверждают, что TClass
является ссылочным типом, поэтому вы можете получить доступ к Свойствам, а TKey
- это то, что реализует нормальное сравнение.
Чтобы создать объекты для класса, у нас есть две функции Factory. Обеим функциям нужен KeySelector, аналогичный множеству селекторов ключей, которые вы можете найти в LINQ. Функция KeySelector - это функция, которая сообщит вам, какое свойство должно использоваться в ваших сравнениях. Он похож на KeySelector в функции Enumerable.Where
.
Вторая функция Create дает возможность предоставить нестандартный компаратор, снова похожий на множество функций в классе Enumerable:
public static IComparer<TClass> Create(Func<TClass, TKey> keySelector)
{ // call the other Create function, with the default TKey comparer
return Create(keySelector, Comparer<TKey>.Default);
}
public static IComparer<TClass> Create(Func<TClass, TKey> keySelector, IComparer<TKey> comparer)
{ // construct a null value last comparer object
// initialize with the key selector and the key comparer
return new NullValueLastComparer<TClass, TKey>()
{
KeySelector = keySelector,
KeyComparer = comparer,
};
}
Я использую частный конструктор. Только статические классы create могут создавать нулевое значение последнего компаратора
private NullValueLastComparer() { }
Два свойства: селектор клавиш и компаратор:
private Func<TClass, TKey> KeySelector { get; set; }
private IComparer<TKey> KeyComparer { get; set; }
Фактическая функция сравнения. Он будет использовать KeySelector для получения значений
которые должны быть сопоставлены, и сравнивает их так, что последнее значение будет последним.
public int Compare(TClass x, TClass y)
{
if (Object.ReferenceEquals(x, null))
throw new ArgumentNullException(nameof(x));
if (Object.ReferenceEquals(y, null)
throw new ArgumentNullException(nameof(y));
// get the values to compare
TKey keyX = KeySelector(x);
TKey keyY = KeySelector(y);
return this.Compare(keyX, keyY);
}
Частная функция, которая сравнивает Ключи таким образом, что нулевые значения будут последними
private int Compare(TKey x, TKey y)
{ // compare such that null values last, or if both not null, use IComparable
if (Object.ReferenceEquals(x, null))
{
if (Object.ReferenceEquals(y, null))
{ // both null
return 0;
}
else
{ // x null, y not null => x follows y
return +1;
}
}
else
{ // x not null
if (Object.ReferenceEquals(y, null))
{ // x not null; y null: x precedes y
return -1;
}
else
{
return this.KeyComparer.Compare(x, y);
}
}
}
}
Применение:
class Person
{
public string FirstName {get; set;}
public string FamilyName {get; set;}
}
// create a comparer that will put Persons without firstName last:
IComparer<Person> myComparer =
NullValueLastComparer<Person, string>.Create(person => person.FirstName);
Person person1 = ...;
Person person2 = ...;
int compareResult = myComparer.Compare(person1, person2);
Это сравнение будет сравнивать людей. Когда сравниваются два Лица, это займет person.FirstName для обоих лиц, и будет помещать одно без FirstName как последнее.
Использование в сложной инструкции LINQ.
Обратите внимание, что во время компиляции выполняется полная проверка типов.
IEnumerable<Person> myPersonCollection = ...
var sortedPersons = myPersonCollection
.OrderBy(person => person, myComparer)
.ThenBy(person => person.LastName)
.Select(person => ...)
.ToDictonary(...)
Ответ 4
Для динамически построенного порядка. По выражению, подобному этому list.OrderBy("NAME DESC").ToList()
, вы можете использовать следующий метод расширения вспомогательного запроса.
Использование
Прежде всего, мы проверяем, чтобы имя свойства существовало в данном классе. Если мы не будем проверять, это приведет к исключению времени выполнения.
Затем мы используем либо OrderByProperty
, либо OrderByPropertyDescending
.
string orderBy = "Name";
if (QueryHelper.PropertyExists<User>(orderBy))
{
list = list.OrderByProperty(orderBy);
- OR -
list = list.OrderByPropertyDescending(orderBy);
}
Вот реальное использование в моего проекта в GitHub.
public static class QueryHelper
{
private static readonly MethodInfo OrderByMethod =
typeof (Queryable).GetMethods().Single(method =>
method.Name == "OrderBy" && method.GetParameters().Length == 2);
private static readonly MethodInfo OrderByDescendingMethod =
typeof (Queryable).GetMethods().Single(method =>
method.Name == "OrderByDescending" && method.GetParameters().Length == 2);
public static bool PropertyExists<T>(string propertyName)
{
return typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase |
BindingFlags.Public | BindingFlags.Instance) != null;
}
public static IQueryable<T> OrderByProperty<T>(
this IQueryable<T> source, string propertyName)
{
if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase |
BindingFlags.Public | BindingFlags.Instance) == null)
{
return null;
}
ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
MethodInfo genericMethod =
OrderByMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
object ret = genericMethod.Invoke(null, new object[] {source, lambda});
return (IQueryable<T>) ret;
}
public static IQueryable<T> OrderByPropertyDescending<T>(
this IQueryable<T> source, string propertyName)
{
if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase |
BindingFlags.Public | BindingFlags.Instance) == null)
{
return null;
}
ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
MethodInfo genericMethod =
OrderByDescendingMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
object ret = genericMethod.Invoke(null, new object[] {source, lambda});
return (IQueryable<T>) ret;
}
}