在动态Linq Order By上保留NULL行

我正在使用下面的代码片段动态地订购我的Linq查询并且效果很好。 我在reflection或复杂的linq查询方面不是很出色但是我需要一种方法,当使用升序时,NULL值是最后的,反之亦然。

因此,如果我的属性名称是一个整数且列值为1,3,5,那么所有NULL行都将在结尾处,而不是默认情况下的开头。 我可以添加什么来表达这种情况?

此代码适用于entity framework,仍然需要进行NULL比较。

list.OrderBy("NAME DESC").ToList() 

  public static class OrderByHelper { public static IOrderedQueryable ThenBy(this IEnumerable enumerable, string orderBy) { return enumerable.AsQueryable().ThenBy(orderBy); } public static IOrderedQueryable ThenBy(this IQueryable collection, string orderBy) { if (string.IsNullOrWhiteSpace(orderBy)) orderBy = "ID DESC"; IOrderedQueryable orderedQueryable = null; foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, false)) orderedQueryable = ApplyOrderBy(collection, orderByInfo); return orderedQueryable; } public static IOrderedQueryable OrderBy(this IEnumerable enumerable, string orderBy) { return enumerable.AsQueryable().OrderBy(orderBy); } public static IOrderedQueryable OrderBy(this IQueryable collection, string orderBy) { if (string.IsNullOrWhiteSpace(orderBy)) orderBy = "ID DESC"; IOrderedQueryable orderedQueryable = null; foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, true)) orderedQueryable = ApplyOrderBy(collection, orderByInfo); return orderedQueryable; } private static IOrderedQueryable ApplyOrderBy(IQueryable 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) { if (orderByInfo.Direction == SortDirection.Ascending) methodName = "ThenBy"; else methodName = "ThenByDescending"; } else { if (orderByInfo.Direction == SortDirection.Ascending) methodName = "OrderBy"; else methodName = "OrderByDescending"; } return (IOrderedQueryable)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 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 } 

它相对简单。 对于每个传递的排序选择器,该方法执行以下操作之一:

 .OrderBy(x => x.Member) .ThenBy(x => x.Member) .OrderByDescending(x => x.Member) .ThenByDescendiong(x => x.Member) 

x.Member类型是引用类型或可空值类型时,可以通过以下表达式使用相同方向预排序来实现所需的行为

 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) 

即预订表达式的原始方法,然后是原始表达式的ThenBy(Descending)

这是实施:

 public static class OrderByHelper { public static IOrderedQueryable ThenBy(this IEnumerable source, string orderBy) { return source.AsQueryable().ThenBy(orderBy); } public static IOrderedQueryable ThenBy(this IQueryable source, string orderBy) { return OrderBy(source, orderBy, false); } public static IOrderedQueryable OrderBy(this IEnumerable source, string orderBy) { return source.AsQueryable().OrderBy(orderBy); } public static IOrderedQueryable OrderBy(this IQueryable source, string orderBy) { return OrderBy(source, orderBy, true); } private static IOrderedQueryable OrderBy(IQueryable 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)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 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 } } 

一种方法是将一个额外的表达式传递给方法中的null测试,并在另一个OrderBy / ThenBy子句中使用它。

将生成两个OrderBy子句 – 第一个将在nullOrder ,而第二个将在实际属性上。

 private static IOrderedQueryable ApplyOrderBy(IQueryable collection, OrderByInfo orderByInfo, Expression> nullOrder) { ... if (!orderByInfo.Initial && collection is IOrderedQueryable) { 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)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)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检查器。 为非可空字段传递null应该有效。 您可以构建它们一次,并根据需要传递:

 static readonly Expression> NullStringOrder = s => s == null ? 1 : 0; static readonly Expression> NullIntOrder = i => !i.HasValue ? 1 : 0; static readonly Expression> NullLongOrder = i => !i.HasValue ? 1 : 0; 

我的方法是创建一个实现IComparer的generics类。 这样,您可以在所有LINQ语句中使用具有非默认比较器的类。 优点是您将在编译时进行完整类型检查。 您不能命名无法比较或不能为null的属性

 class NullValueLastComparer : IComparer where TClass : class where TKey : IComparable { 

此generics类有两个Type参数:要比较的类,以及要与之比较的属性的类型。 where子句声明TClass是引用类型,因此您可以访问Properties,而TKey是实现正常比较的东西。

要为类创建对象,我们有两个Factory函数。 这两个函数都需要一个KeySelector,类似于LINQ中可以找到的许多Key Selectors。 KeySelector函数是告诉您在比较中必须使用哪个属性的函数。 它类似于Enumerable.Where函数中的KeySelector。

第二个Create函数使您可以提供非默认比较器,类似于Enumerable类中的许多函数:

  public static IComparer Create(Func keySelector) { // call the other Create function, with the default TKey comparer return Create(keySelector, Comparer.Default); } public static IComparer Create(Func keySelector, IComparer comparer) { // construct a null value last comparer object // initialize with the key selector and the key comparer return new NullValueLastComparer() { KeySelector = keySelector, KeyComparer = comparer, }; } 

我使用私有构造函数。 只有静态创建类可以构造最后比较器的空值

  private NullValueLastComparer() { } 

两个属性:键选择器和比较器:

  private Func KeySelector { get; set; } private IComparer KeyComparer { get; set; } 

实际比较function。 它将使用KeySelector获取必须比较的值,并将它们进行比较,使得null值将是最后一个。

  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); } 

用于比较Keys的私有函数,使得null值将是最后一个

  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 myComparer = NullValueLastComparer.Create(person => person.FirstName); Person person1 = ...; Person person2 = ...; int compareResult = myComparer.Compare(person1, person2); 

这种比较将比较人。 比较两个人时,两个人都需要person.FirstName,并且将没有FirstName的人作为最后一个。

在复杂的LINQ语句中使用。 请注意,在编译时有完整的类型检查。

 IEnumerable myPersonCollection = ... var sortedPersons = myPersonCollection .OrderBy(person => person, myComparer) .ThenBy(person => person.LastName) .Select(person => ...) .ToDictonary(...) 

对于动态构造的Order By表达式,list.OrderBy("NAME DESC").ToList() ,您可以使用以下查询辅助扩展方法

用法

首先,我们检查以确保给定类中存在属性名称。 如果我们不检查,它将抛出运行时exception。

然后我们使用OrderByPropertyOrderByPropertyDescending

 string orderBy = "Name"; if (QueryHelper.PropertyExists(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(string propertyName) { return typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) != null; } public static IQueryable OrderByProperty( this IQueryable 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) ret; } public static IQueryable OrderByPropertyDescending( this IQueryable 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) ret; } }