Skip to content

Commit 436ee2e

Browse files
[API Implementation]: Add Order and OrderDescending to Enumerable and Queryable (#70525)
* Implement proposal for `System.Linq` * Implement proposal for `System.Linq.Queryable` * Add documentation * Merge branch 'main' into issue-67194 * Add more test cases * Apply suggestions Co-Authored-By: Eirik Tsarpalis <[email protected]> * Remove unneccesary keyless stuff * Add more Queryable test cases * Eliminate `keySelector` null-check in ctor * Add null checks to `ThenBy` * Revert "Eliminate `keySelector` null-check in ctor" This reverts commit 879421b. * Add tests for CreateOrderedEnumerable * Apply suggestions * Fix null checks * Fix null checks * Update src/libraries/System.Linq/src/System/Linq/OrderBy.cs * Update src/libraries/System.Linq/src/System/Linq/OrderBy.cs Co-authored-by: Eirik Tsarpalis <[email protected]> Co-authored-by: Eirik Tsarpalis <[email protected]>
1 parent bb36771 commit 436ee2e

13 files changed

+1082
-5
lines changed

src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ public static partial class Queryable
134134
public static System.Linq.IOrderedQueryable<TSource> OrderByDescending<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TKey>? comparer) { throw null; }
135135
public static System.Linq.IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector) { throw null; }
136136
public static System.Linq.IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TKey>? comparer) { throw null; }
137+
public static System.Linq.IOrderedQueryable<T> OrderDescending<T>(this System.Linq.IQueryable<T> source) { throw null; }
138+
public static System.Linq.IOrderedQueryable<T> OrderDescending<T>(this System.Linq.IQueryable<T> source, System.Collections.Generic.IComparer<T> comparer) { throw null; }
139+
public static System.Linq.IOrderedQueryable<T> Order<T>(this System.Linq.IQueryable<T> source) { throw null; }
140+
public static System.Linq.IOrderedQueryable<T> Order<T>(this System.Linq.IQueryable<T> source, System.Collections.Generic.IComparer<T> comparer) { throw null; }
137141
public static System.Linq.IQueryable<TSource> Prepend<TSource>(this System.Linq.IQueryable<TSource> source, TSource element) { throw null; }
138142
public static System.Linq.IQueryable<TSource> Reverse<TSource>(this System.Linq.IQueryable<TSource> source) { throw null; }
139143
public static System.Linq.IQueryable<TResult> SelectMany<TSource, TResult>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, System.Collections.Generic.IEnumerable<TResult>>> selector) { throw null; }

src/libraries/System.Linq.Queryable/src/System/Linq/CachedReflection.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,12 +530,36 @@ public static MethodInfo OfType_TResult_1(Type TResult) =>
530530
(s_OfType_TResult_1 ??= new Func<IQueryable, IQueryable<object>>(Queryable.OfType<object>).GetMethodInfo().GetGenericMethodDefinition())
531531
.MakeGenericMethod(TResult);
532532

533+
private static MethodInfo? s_Order_T_1;
534+
535+
public static MethodInfo Order_T_1(Type T) =>
536+
(s_Order_T_1 ??= new Func<IQueryable<object>, IOrderedQueryable<object>>(Queryable.Order).GetMethodInfo().GetGenericMethodDefinition())
537+
.MakeGenericMethod(T);
538+
539+
private static MethodInfo? s_Order_T_2;
540+
541+
public static MethodInfo Order_T_2(Type T) =>
542+
(s_Order_T_2 ??= new Func<IQueryable<object>, IComparer<object>, IOrderedQueryable<object>>(Queryable.Order).GetMethodInfo().GetGenericMethodDefinition())
543+
.MakeGenericMethod(T);
544+
533545
private static MethodInfo? s_OrderBy_TSource_TKey_2;
534546

535547
public static MethodInfo OrderBy_TSource_TKey_2(Type TSource, Type TKey) =>
536548
(s_OrderBy_TSource_TKey_2 ??= new Func<IQueryable<object>, Expression<Func<object, object>>, IOrderedQueryable<object>>(Queryable.OrderBy).GetMethodInfo().GetGenericMethodDefinition())
537549
.MakeGenericMethod(TSource, TKey);
538550

551+
private static MethodInfo? s_OrderDescending_T_1;
552+
553+
public static MethodInfo OrderDescending_T_1(Type T) =>
554+
(s_OrderDescending_T_1 ??= new Func<IQueryable<object>, IOrderedQueryable<object>>(Queryable.OrderDescending).GetMethodInfo().GetGenericMethodDefinition())
555+
.MakeGenericMethod(T);
556+
557+
private static MethodInfo? s_OrderDescending_T_2;
558+
559+
public static MethodInfo OrderDescending_T_2(Type T) =>
560+
(s_OrderDescending_T_2 ??= new Func<IQueryable<object>, IComparer<object>, IOrderedQueryable<object>>(Queryable.OrderDescending).GetMethodInfo().GetGenericMethodDefinition())
561+
.MakeGenericMethod(T);
562+
539563
private static MethodInfo? s_OrderBy_TSource_TKey_3;
540564

541565
public static MethodInfo OrderBy_TSource_TKey_3(Type TSource, Type TKey) =>

src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,81 @@ public static IQueryable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(this
241241
CachedReflectionInfo.GroupJoin_TOuter_TInner_TKey_TResult_6(typeof(TOuter), typeof(TInner), typeof(TKey), typeof(TResult)), outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer<TKey>))));
242242
}
243243

244+
/// <summary>
245+
/// Sorts the elements of a sequence in ascending order.
246+
/// </summary>
247+
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
248+
/// <param name="source">A sequence of values to order.</param>
249+
/// <returns>An <see cref="IOrderedEnumerable{TElement}"/> whose elements are sorted.</returns>
250+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
251+
/// <remarks>
252+
/// This method has at least one parameter of type <see cref="Expression{TDelegate}"/> whose type argument is one
253+
/// of the <see cref="Func{T,TResult}"/> types.
254+
/// For these parameters, you can pass in a lambda expression and it will be compiled to an <see cref="Expression{TDelegate}"/>.
255+
///
256+
/// The <see cref="Order{T}(IQueryable{T})"/> method generates a <see cref="MethodCallExpression"/> that represents
257+
/// calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/> itself as a constructed generic method.
258+
/// It then passes the <see cref="MethodCallExpression"/> to the <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> method
259+
/// of the <see cref="IQueryProvider"/> represented by the <see cref="IQueryable.Provider"/> property of the <paramref name="source"/>
260+
/// parameter. The result of calling <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> is cast to
261+
/// type <see cref="IOrderedQueryable{T}"/> and returned.
262+
///
263+
/// The query behavior that occurs as a result of executing an expression tree
264+
/// that represents calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/>
265+
/// depends on the implementation of the <paramref name="source"/> parameter.
266+
/// The expected behavior is that it sorts the elements of <paramref name="source"/> by itself.
267+
/// </remarks>
268+
[DynamicDependency("Order`1", typeof(Enumerable))]
269+
public static IOrderedQueryable<T> Order<T>(this IQueryable<T> source)
270+
{
271+
ArgumentNullException.ThrowIfNull(source);
272+
273+
return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(
274+
Expression.Call(
275+
null,
276+
CachedReflectionInfo.Order_T_1(typeof(T)),
277+
source.Expression
278+
));
279+
}
280+
281+
/// <summary>
282+
/// Sorts the elements of a sequence in ascending order.
283+
/// </summary>
284+
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
285+
/// <param name="source">A sequence of values to order.</param>
286+
/// <param name="comparer">An <see cref="IComparer{T}"/> to compare elements.</param>
287+
/// <returns>An <see cref="IOrderedEnumerable{TElement}"/> whose elements are sorted.</returns>
288+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
289+
/// <remarks>
290+
/// This method has at least one parameter of type <see cref="Expression{TDelegate}"/> whose type argument is one
291+
/// of the <see cref="Func{T,TResult}"/> types.
292+
/// For these parameters, you can pass in a lambda expression and it will be compiled to an <see cref="Expression{TDelegate}"/>.
293+
///
294+
/// The <see cref="Order{T}(IQueryable{T})"/> method generates a <see cref="MethodCallExpression"/> that represents
295+
/// calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/> itself as a constructed generic method.
296+
/// It then passes the <see cref="MethodCallExpression"/> to the <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> method
297+
/// of the <see cref="IQueryProvider"/> represented by the <see cref="IQueryable.Provider"/> property of the <paramref name="source"/>
298+
/// parameter. The result of calling <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> is cast to
299+
/// type <see cref="IOrderedQueryable{T}"/> and returned.
300+
///
301+
/// The query behavior that occurs as a result of executing an expression tree
302+
/// that represents calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/>
303+
/// depends on the implementation of the <paramref name="source"/> parameter.
304+
/// The expected behavior is that it sorts the elements of <paramref name="source"/> by itself.
305+
/// </remarks>
306+
[DynamicDependency("Order`1", typeof(Enumerable))]
307+
public static IOrderedQueryable<T> Order<T>(this IQueryable<T> source, IComparer<T> comparer)
308+
{
309+
ArgumentNullException.ThrowIfNull(source);
310+
311+
return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(
312+
Expression.Call(
313+
null,
314+
CachedReflectionInfo.Order_T_2(typeof(T)),
315+
source.Expression, Expression.Constant(comparer, typeof(IComparer<T>))
316+
));
317+
}
318+
244319
[DynamicDependency("OrderBy`2", typeof(Enumerable))]
245320
public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector)
246321
{
@@ -269,6 +344,81 @@ public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<
269344
));
270345
}
271346

347+
/// <summary>
348+
/// Sorts the elements of a sequence in descending order.
349+
/// </summary>
350+
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
351+
/// <param name="source">A sequence of values to order.</param>
352+
/// <returns>An <see cref="IOrderedEnumerable{TElement}"/> whose elements are sorted.</returns>
353+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
354+
/// <remarks>
355+
/// This method has at least one parameter of type <see cref="Expression{TDelegate}"/> whose type argument is one
356+
/// of the <see cref="Func{T,TResult}"/> types.
357+
/// For these parameters, you can pass in a lambda expression and it will be compiled to an <see cref="Expression{TDelegate}"/>.
358+
///
359+
/// The <see cref="Order{T}(IQueryable{T})"/> method generates a <see cref="MethodCallExpression"/> that represents
360+
/// calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/> itself as a constructed generic method.
361+
/// It then passes the <see cref="MethodCallExpression"/> to the <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> method
362+
/// of the <see cref="IQueryProvider"/> represented by the <see cref="IQueryable.Provider"/> property of the <paramref name="source"/>
363+
/// parameter. The result of calling <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> is cast to
364+
/// type <see cref="IOrderedQueryable{T}"/> and returned.
365+
///
366+
/// The query behavior that occurs as a result of executing an expression tree
367+
/// that represents calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/>
368+
/// depends on the implementation of the <paramref name="source"/> parameter.
369+
/// The expected behavior is that it sorts the elements of <paramref name="source"/> by itself.
370+
/// </remarks>
371+
[DynamicDependency("OrderDescending`1", typeof(Enumerable))]
372+
public static IOrderedQueryable<T> OrderDescending<T>(this IQueryable<T> source)
373+
{
374+
ArgumentNullException.ThrowIfNull(source);
375+
376+
return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(
377+
Expression.Call(
378+
null,
379+
CachedReflectionInfo.OrderDescending_T_1(typeof(T)),
380+
source.Expression
381+
));
382+
}
383+
384+
/// <summary>
385+
/// Sorts the elements of a sequence in descending order.
386+
/// </summary>
387+
/// <typeparam name="T">The type of the elements of <paramref name="source"/>.</typeparam>
388+
/// <param name="source">A sequence of values to order.</param>
389+
/// <param name="comparer">An <see cref="IComparer{T}"/> to compare elements.</param>
390+
/// <returns>An <see cref="IOrderedEnumerable{TElement}"/> whose elements are sorted.</returns>
391+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
392+
/// <remarks>
393+
/// This method has at least one parameter of type <see cref="Expression{TDelegate}"/> whose type argument is one
394+
/// of the <see cref="Func{T,TResult}"/> types.
395+
/// For these parameters, you can pass in a lambda expression and it will be compiled to an <see cref="Expression{TDelegate}"/>.
396+
///
397+
/// The <see cref="Order{T}(IQueryable{T})"/> method generates a <see cref="MethodCallExpression"/> that represents
398+
/// calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/> itself as a constructed generic method.
399+
/// It then passes the <see cref="MethodCallExpression"/> to the <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> method
400+
/// of the <see cref="IQueryProvider"/> represented by the <see cref="IQueryable.Provider"/> property of the <paramref name="source"/>
401+
/// parameter. The result of calling <see cref="IQueryProvider.CreateQuery{TElement}(Expression)"/> is cast to
402+
/// type <see cref="IOrderedQueryable{T}"/> and returned.
403+
///
404+
/// The query behavior that occurs as a result of executing an expression tree
405+
/// that represents calling <see cref="Enumerable.Order{T}(IEnumerable{T})"/>
406+
/// depends on the implementation of the <paramref name="source"/> parameter.
407+
/// The expected behavior is that it sorts the elements of <paramref name="source"/> by itself.
408+
/// </remarks>
409+
[DynamicDependency("OrderDescending`1", typeof(Enumerable))]
410+
public static IOrderedQueryable<T> OrderDescending<T>(this IQueryable<T> source, IComparer<T> comparer)
411+
{
412+
ArgumentNullException.ThrowIfNull(source);
413+
414+
return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(
415+
Expression.Call(
416+
null,
417+
CachedReflectionInfo.OrderDescending_T_2(typeof(T)),
418+
source.Expression, Expression.Constant(comparer, typeof(IComparer<T>))
419+
));
420+
}
421+
272422
[DynamicDependency("OrderByDescending`2", typeof(Enumerable))]
273423
public static IOrderedQueryable<TSource> OrderByDescending<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector)
274424
{
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Xunit;
6+
7+
namespace System.Linq.Tests
8+
{
9+
public sealed class OrderDescendingTests : EnumerableBasedTests
10+
{
11+
[Fact]
12+
public void FirstAndLastAreDuplicatesCustomComparer()
13+
{
14+
string[] source = { "Prakash", "Alpha", "DAN", "dan", "Prakash" };
15+
string[] expected = { "Prakash", "Prakash", "DAN", "dan", "Alpha" };
16+
17+
Assert.Equal(expected, source.AsQueryable().OrderDescending(StringComparer.OrdinalIgnoreCase));
18+
}
19+
20+
[Fact]
21+
public void FirstAndLastAreDuplicatesNullPassedAsComparer()
22+
{
23+
int[] source = { 5, 1, 3, 2, 5 };
24+
int[] expected = { 5, 5, 3, 2, 1 };
25+
26+
Assert.Equal(expected, source.AsQueryable().OrderDescending(null));
27+
}
28+
29+
[Fact]
30+
public void NullSource()
31+
{
32+
IQueryable<int> source = null;
33+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.OrderDescending());
34+
}
35+
36+
[Fact]
37+
public void NullSourceComparer()
38+
{
39+
IQueryable<int> source = null;
40+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.OrderDescending(Comparer<int>.Default));
41+
}
42+
43+
[Fact]
44+
public void OrderDescending1()
45+
{
46+
var count = (new int[] { 0, 1, 2 }).AsQueryable().OrderDescending().Count();
47+
Assert.Equal(3, count);
48+
}
49+
50+
[Fact]
51+
public void OrderDescending2()
52+
{
53+
var count = (new int[] { 0, 1, 2 }).AsQueryable().OrderDescending(Comparer<int>.Default).Count();
54+
Assert.Equal(3, count);
55+
}
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Xunit;
6+
7+
namespace System.Linq.Tests
8+
{
9+
public sealed class OrderTests : EnumerableBasedTests
10+
{
11+
[Fact]
12+
public void FirstAndLastAreDuplicatesCustomComparer()
13+
{
14+
string[] source = { "Prakash", "Alpha", "dan", "DAN", "Prakash" };
15+
string[] expected = { "Alpha", "dan", "DAN", "Prakash", "Prakash" };
16+
17+
Assert.Equal(expected, source.AsQueryable().Order(StringComparer.OrdinalIgnoreCase));
18+
}
19+
20+
[Fact]
21+
public void FirstAndLastAreDuplicatesNullPassedAsComparer()
22+
{
23+
int[] source = { 5, 1, 3, 2, 5 };
24+
int[] expected = { 1, 2, 3, 5, 5 };
25+
26+
Assert.Equal(expected, source.AsQueryable().Order(null));
27+
}
28+
29+
[Fact]
30+
public void NullSource()
31+
{
32+
IQueryable<int> source = null;
33+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Order());
34+
}
35+
36+
[Fact]
37+
public void NullSourceComparer()
38+
{
39+
IQueryable<int> source = null;
40+
AssertExtensions.Throws<ArgumentNullException>("source", () => source.Order(Comparer<int>.Default));
41+
}
42+
43+
[Fact]
44+
public void Order1()
45+
{
46+
var count = (new int[] { 0, 1, 2 }).AsQueryable().Order().Count();
47+
Assert.Equal(3, count);
48+
}
49+
50+
[Fact]
51+
public void Order2()
52+
{
53+
var count = (new int[] { 0, 1, 2 }).AsQueryable().Order(Comparer<int>.Default).Count();
54+
Assert.Equal(3, count);
55+
}
56+
}
57+
}

src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
<Compile Include="OfTypeTests.cs" />
3535
<Compile Include="OrderByDescendingTests.cs" />
3636
<Compile Include="OrderByTests.cs" />
37+
<Compile Include="OrderDescendingTests.cs" />
38+
<Compile Include="OrderTests.cs" />
3739
<Compile Include="Queryable.cs" />
3840
<Compile Include="QueryFromExpressionTests.cs" />
3941
<Compile Include="ReverseTests.cs" />

src/libraries/System.Linq.Queryable/tests/TrimCompatibilityTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static void CachedReflectionInfoMethodsNoAnnotations()
6262
.Where(m => m.GetParameters().Length > 0);
6363

6464
// If you are adding a new method to this class, ensure the method meets these requirements
65-
Assert.Equal(131, methods.Count());
65+
Assert.Equal(135, methods.Count());
6666
foreach (MethodInfo method in methods)
6767
{
6868
ParameterInfo[] parameters = method.GetParameters();

0 commit comments

Comments
 (0)