Skip to content

Commit 40d3b73

Browse files
committed
Added ThenBy (#62)
1 parent 4eb3dc9 commit 40d3b73

File tree

5 files changed

+146
-23
lines changed

5 files changed

+146
-23
lines changed

src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs

+65-2
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ public static IOrderedQueryable OrderBy([NotNull] this IQueryable source, [NotNu
574574

575575
ParameterExpression[] parameters = { Expression.Parameter(source.ElementType, "") };
576576
ExpressionParser parser = new ExpressionParser(parameters, ordering, args);
577-
IEnumerable<DynamicOrdering> dynamicOrderings = parser.ParseOrdering();
577+
IList<DynamicOrdering> dynamicOrderings = parser.ParseOrdering();
578578

579579
Expression queryExpr = source.Expression;
580580

@@ -1142,7 +1142,70 @@ public static IQueryable TakeWhile([NotNull] this IQueryable source, [NotNull] s
11421142

11431143
return CreateQuery(_takeWhilePredicate, source, lambda);
11441144
}
1145-
#endregion TakeWhile
1145+
#endregion TakeWhile
1146+
1147+
#region ThenBy
1148+
/// <summary>
1149+
/// Performs a subsequent ordering of the elements in a sequence in ascending order according to a key.
1150+
/// </summary>
1151+
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
1152+
/// <param name="source">A sequence of values to order.</param>
1153+
/// <param name="ordering">An expression string to indicate values to order by.</param>
1154+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
1155+
/// <returns>A <see cref="IOrderedQueryable{T}"/> whose elements are sorted according to the specified <paramref name="ordering"/>.</returns>
1156+
/// <example>
1157+
/// <code>
1158+
/// <![CDATA[
1159+
/// var result = queryable.OrderBy<User>("LastName");
1160+
/// var resultSingle = result.ThenBy<User>("NumberProperty");
1161+
/// var resultSingleDescending = result.ThenBy<User>("NumberProperty DESC");
1162+
/// var resultMultiple = result.ThenBy<User>("NumberProperty, StringProperty");
1163+
/// ]]>
1164+
/// </code>
1165+
/// </example>
1166+
public static IOrderedQueryable<TSource> ThenBy<TSource>([NotNull] this IOrderedQueryable<TSource> source, [NotNull] string ordering, params object[] args)
1167+
{
1168+
return (IOrderedQueryable<TSource>)ThenBy((IOrderedQueryable)source, ordering, args);
1169+
}
1170+
1171+
/// <summary>
1172+
/// Performs a subsequent ordering of the elements in a sequence in ascending order according to a key.
1173+
/// </summary>
1174+
/// <param name="source">A sequence of values to order.</param>
1175+
/// <param name="ordering">An expression string to indicate values to order by.</param>
1176+
/// <param name="args">An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.</param>
1177+
/// <returns>A <see cref="IQueryable"/> whose elements are sorted according to the specified <paramref name="ordering"/>.</returns>
1178+
/// <example>
1179+
/// <code>
1180+
/// var result = queryable.OrderBy("LastName");
1181+
/// var resultSingle = result.OrderBy("NumberProperty");
1182+
/// var resultSingleDescending = result.OrderBy("NumberProperty DESC");
1183+
/// var resultMultiple = result.OrderBy("NumberProperty, StringProperty DESC");
1184+
/// </code>
1185+
/// </example>
1186+
public static IOrderedQueryable ThenBy([NotNull] this IOrderedQueryable source, [NotNull] string ordering, params object[] args)
1187+
{
1188+
Check.NotNull(source, nameof(source));
1189+
Check.NotEmpty(ordering, nameof(ordering));
1190+
1191+
ParameterExpression[] parameters = { Expression.Parameter(source.ElementType, "") };
1192+
ExpressionParser parser = new ExpressionParser(parameters, ordering, args);
1193+
IList<DynamicOrdering> dynamicOrderings = parser.ParseOrdering(forceThenBy: true);
1194+
1195+
Expression queryExpr = source.Expression;
1196+
1197+
foreach (DynamicOrdering dynamicOrdering in dynamicOrderings)
1198+
{
1199+
queryExpr = Expression.Call(
1200+
typeof(Queryable), dynamicOrdering.MethodName,
1201+
new[] { source.ElementType, dynamicOrdering.Selector.Type },
1202+
queryExpr, Expression.Quote(Expression.Lambda(dynamicOrdering.Selector, parameters)));
1203+
}
1204+
1205+
var optimized = OptimizeExpression(queryExpr);
1206+
return (IOrderedQueryable)source.Provider.CreateQuery(optimized);
1207+
}
1208+
#endregion OrderBy
11461209

11471210
#region Where
11481211
/// <summary>

src/System.Linq.Dynamic.Core/ExpressionParser.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ interface IEnumerableSignatures
141141
void SelectMany(object selector);
142142
void OrderBy(object selector);
143143
void OrderByDescending(object selector);
144+
void ThenBy(object selector);
145+
void ThenByDescending(object selector);
144146
void Contains(object selector);
145147
void Skip(int count);
146148
void SkipWhile(bool predicate);
@@ -330,7 +332,7 @@ public Expression Parse(Type resultType, bool createParameterCtor)
330332
}
331333

332334
#pragma warning disable 0219
333-
public IEnumerable<DynamicOrdering> ParseOrdering()
335+
public IList<DynamicOrdering> ParseOrdering(bool forceThenBy = false)
334336
{
335337
var orderings = new List<DynamicOrdering>();
336338
while (true)
@@ -348,10 +350,10 @@ public IEnumerable<DynamicOrdering> ParseOrdering()
348350
}
349351

350352
string methodName;
351-
if (orderings.Count == 0)
352-
methodName = ascending ? methodOrderBy : methodOrderByDescending;
353-
else
353+
if (forceThenBy || orderings.Count > 0)
354354
methodName = ascending ? methodThenBy : methodThenByDescending;
355+
else
356+
methodName = ascending ? methodOrderBy : methodOrderByDescending;
355357

356358
orderings.Add(new DynamicOrdering { Selector = expr, Ascending = ascending, MethodName = methodName });
357359

@@ -1386,7 +1388,7 @@ Expression ParseAggregate(Expression instance, Type elementType, string methodNa
13861388
throw ParseError(errorPos, Res.NoApplicableAggregate, methodName);
13871389

13881390
Type[] typeArgs;
1389-
if (new[] { "Min", "Max", "Select", "OrderBy", "OrderByDescending" }.Contains(signature.Name))
1391+
if (new[] { "Min", "Max", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending" }.Contains(signature.Name))
13901392
{
13911393
typeArgs = new[] { elementType, args[0].Type };
13921394
}

src/System.Linq.Dynamic.Core/project.json

+15-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "1.0.6.10",
2+
"version": "1.0.6.11",
33
"title": "System.Linq.Dynamic.Core",
44
"description": "This is a .NET Core port of the the Microsoft assembly for the .Net 4.0 Dynamic language functionality.",
55
"authors": [ "Microsoft", "Scott Guthrie", "King Wilder", "Nathan Arnott", "Stef Heyenrath" ],
@@ -14,7 +14,7 @@
1414
},
1515
"projectUrl": "https://github.com/StefH/System.Linq.Dynamic.Core",
1616
"licenseUrl": "https://github.com/StefH/System.Linq.Dynamic.Core/blob/master/licence.txt",
17-
"releaseNotes": "Add [] to DynamicClass"
17+
"releaseNotes": "Added support for ThenBy."
1818
},
1919

2020
"buildOptions": {
@@ -100,19 +100,19 @@
100100
"dotnet5.4"
101101
],
102102
"dependencies": {
103-
"System.Collections.Concurrent": "4.0.12",
104-
"System.ComponentModel.TypeConverter": "4.1.0",
105-
"System.Diagnostics.Debug": "4.0.11",
106-
"System.Diagnostics.TraceSource": "4.0.0",
107-
"System.Dynamic.Runtime": "4.0.11",
108-
"System.Globalization": "4.0.11",
109-
"System.Linq.Expressions": "4.1.0",
110-
"System.Linq.Queryable": "4.0.1",
111-
"System.ObjectModel": "4.0.12",
112-
"System.Reflection.Emit": "4.0.1",
113-
"System.Reflection.TypeExtensions": "4.1.0",
114-
"System.Runtime.Extensions": "4.1.0",
115-
"System.Threading": "4.0.11"
103+
"System.Collections.Concurrent": "4.3.0",
104+
"System.ComponentModel.TypeConverter": "4.3.0",
105+
"System.Diagnostics.Debug": "4.3.0",
106+
"System.Diagnostics.TraceSource": "4.3.0",
107+
"System.Dynamic.Runtime": "4.3.0",
108+
"System.Globalization": "4.3.0",
109+
"System.Linq.Expressions": "4.3.0",
110+
"System.Linq.Queryable": "4.3.0",
111+
"System.ObjectModel": "4.3.0",
112+
"System.Reflection.Emit": "4.3.0",
113+
"System.Reflection.TypeExtensions": "4.3.0",
114+
"System.Runtime.Extensions": "4.3.0",
115+
"System.Threading": "4.3.0"
116116
}
117117
},
118118
"sl5": {

test/System.Linq.Dynamic.Core.Tests/QueryableTests.OrderBy.cs

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public void OrderBy_Dynamic()
2222
Assert.Equal(testList.OrderBy(x => x.Id).ToArray(), orderById.ToArray());
2323
Assert.Equal(testList.OrderBy(x => x.Profile.Age).ToArray(), orderByAge.ToArray());
2424
Assert.Equal(testList.OrderBy(x => x.Profile.Age).ThenBy(x => x.Id).ToArray(), orderByComplex1.ToArray());
25-
2625
}
2726

2827
[Fact]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Linq.Dynamic.Core.Exceptions;
2+
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
3+
using Xunit;
4+
5+
namespace System.Linq.Dynamic.Core.Tests
6+
{
7+
public partial class QueryableTests
8+
{
9+
[Fact]
10+
public void ThenBy_Dynamic()
11+
{
12+
//Arrange
13+
var testList = User.GenerateSampleModels(100);
14+
var qry = testList.AsQueryable();
15+
16+
//Act
17+
var ordered = qry.OrderBy("Id");
18+
var thenByUserName = ordered.ThenBy("UserName");
19+
var thenByComplex1 = ordered.ThenBy("Profile.Age, Income");
20+
21+
//Assert
22+
Assert.Equal(testList.OrderBy(x => x.Id).ThenBy(x => x.UserName).ToArray(), thenByUserName.ToArray());
23+
Assert.Equal(testList.OrderBy(x => x.Id).ThenBy(x => x.Profile.Age).ThenBy(x => x.Income).ToArray(), thenByComplex1.ToArray());
24+
}
25+
26+
[Fact]
27+
public void ThenBy_Dynamic_AsStringExpression()
28+
{
29+
//Arrange
30+
var testList = User.GenerateSampleModels(100);
31+
var qry = testList.AsQueryable();
32+
33+
//Act
34+
var expected = qry.SelectMany(x => x.Roles.OrderBy(y => y.Name).ThenBy(y => y.Id)).Select(x => x.Name);
35+
var orderById = qry.SelectMany("Roles.OrderBy(Name).ThenBy(Id)").Select("Name");
36+
37+
//Assert
38+
Assert.Equal(expected.ToArray(), orderById.Cast<string>().ToArray());
39+
}
40+
41+
[Fact]
42+
public void ThenBy_Dynamic_Exceptions()
43+
{
44+
//Arrange
45+
var testList = User.GenerateSampleModels(100, allowNullableProfiles: true);
46+
var qry = testList.AsQueryable();
47+
48+
//Act
49+
var ordered = qry.OrderBy("Id");
50+
Assert.Throws<ParseException>(() => ordered.ThenBy("Bad=3"));
51+
Assert.Throws<ParseException>(() => ordered.Where("Id=123"));
52+
53+
Assert.Throws<ArgumentNullException>(() => DynamicQueryableExtensions.ThenBy(null, "Id"));
54+
Assert.Throws<ArgumentNullException>(() => ordered.ThenBy(null));
55+
Assert.Throws<ArgumentException>(() => ordered.ThenBy(""));
56+
Assert.Throws<ArgumentException>(() => ordered.ThenBy(" "));
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)