Skip to content

Commit bd4f680

Browse files
committed
Implemented sorting
1 parent 9890365 commit bd4f680

File tree

9 files changed

+463
-20
lines changed

9 files changed

+463
-20
lines changed

src/HotChocolate/Data/src/Data/Filters/Extensions/QueryableExtensions.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using HotChocolate.Data;
12
using HotChocolate.Data.Filters;
3+
using HotChocolate.Data.Sorting;
24
using HotChocolate.Execution.Processing;
35

46
// ReSharper disable once CheckNamespace
@@ -76,4 +78,120 @@ public static IQueryable<T> Where<T>(this IQueryable<T> queryable, IFilterContex
7678
var predicate = filter.AsPredicate<T>();
7779
return predicate is null ? queryable : queryable.Where(predicate);
7880
}
81+
82+
/// <summary>
83+
/// Applies a sorting context to the queryable.
84+
/// </summary>
85+
/// <param name="queryable">
86+
/// The queryable that shall be sorted.
87+
/// </param>
88+
/// <param name="sorting">
89+
/// The sorting context that shall be applied to the queryable.
90+
/// </param>
91+
/// <typeparam name="T">
92+
/// The type of the queryable.
93+
/// </typeparam>
94+
/// <returns>
95+
/// Returns a queryable that has the sorting applied.
96+
/// </returns>
97+
/// <exception cref="ArgumentNullException">
98+
/// Throws if <paramref name="queryable"/> is <c>null</c> or if <paramref name="sorting"/> is <c>null</c>.
99+
/// </exception>
100+
public static IQueryable<T> Order<T>(this IQueryable<T> queryable, ISortingContext sorting)
101+
{
102+
if (queryable is null)
103+
{
104+
throw new ArgumentNullException(nameof(queryable));
105+
}
106+
107+
if (sorting is null)
108+
{
109+
throw new ArgumentNullException(nameof(sorting));
110+
}
111+
112+
var sortDefinition = sorting.AsSortDefinition<T>();
113+
114+
if (sortDefinition is null || sortDefinition.Operations.Length == 0)
115+
{
116+
return queryable;
117+
}
118+
119+
return queryable.Order(sortDefinition);
120+
}
121+
122+
private static IQueryable<T> Order<T>(this IQueryable<T> queryable, SortDefinition<T> sortDefinition)
123+
{
124+
if (queryable is null)
125+
{
126+
throw new ArgumentNullException(nameof(queryable));
127+
}
128+
129+
if (sortDefinition is null)
130+
{
131+
throw new ArgumentNullException(nameof(sortDefinition));
132+
}
133+
134+
if (sortDefinition.Operations.Length == 0)
135+
{
136+
return queryable;
137+
}
138+
139+
var first = sortDefinition.Operations[0];
140+
var query = first.ApplyOrderBy(queryable);
141+
142+
for (var i = 1; i < sortDefinition.Operations.Length; i++)
143+
{
144+
query = sortDefinition.Operations[i].ApplyThenBy(query);
145+
}
146+
147+
return query;
148+
}
149+
150+
/// <summary>
151+
/// Applies a data context to the queryable.
152+
/// </summary>
153+
/// <param name="queryable">
154+
/// The queryable that shall be projected, filtered and sorted.
155+
/// </param>
156+
/// <param name="dataContext">
157+
/// The data context that shall be applied to the queryable.
158+
/// </param>
159+
/// <typeparam name="T">
160+
/// The type of the queryable.
161+
/// </typeparam>
162+
/// <returns>
163+
/// Returns a queryable that has the data context applied.
164+
/// </returns>
165+
/// <exception cref="ArgumentNullException">
166+
/// Throws if <paramref name="queryable"/> is <c>null</c> or if <paramref name="dataContext"/> is <c>null</c>.
167+
/// </exception>
168+
public static IQueryable<T> Apply<T>(this IQueryable<T> queryable, DataContext<T> dataContext)
169+
{
170+
if (queryable is null)
171+
{
172+
throw new ArgumentNullException(nameof(queryable));
173+
}
174+
175+
if (dataContext is null)
176+
{
177+
throw new ArgumentNullException(nameof(dataContext));
178+
}
179+
180+
if (dataContext.Selector is not null)
181+
{
182+
queryable = queryable.Select(dataContext.Selector);
183+
}
184+
185+
if (dataContext.Predicate is not null)
186+
{
187+
queryable = queryable.Where(dataContext.Predicate);
188+
}
189+
190+
if (dataContext.Sorting is not null)
191+
{
192+
queryable = queryable.Order(dataContext.Sorting);
193+
}
194+
195+
return queryable;
196+
}
79197
}

src/HotChocolate/Data/src/Data/Sorting/Context/ISortingContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public interface ISortingContext
4848
/// <returns>
4949
/// Returns a sort definition that can be used to sort a query.
5050
/// </returns>
51-
SortDefinition<T> AsSortDefinition<T>();
51+
SortDefinition<T>? AsSortDefinition<T>();
5252
}
5353

5454
public delegate TQuery PostSortingAction<TQuery>(bool userDefinedSorting, TQuery query);

src/HotChocolate/Data/src/Data/Sorting/Context/SortingContext.cs

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
using System.Collections.Concurrent;
2+
using System.Collections.Immutable;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
using HotChocolate.Data.Projections.Expressions.Handlers;
16
using HotChocolate.Language;
7+
using HotChocolate.Language.Visitors;
28
using HotChocolate.Resolvers;
39
using HotChocolate.Types;
410
using static HotChocolate.Data.Sorting.Expressions.QueryableSortProvider;
@@ -10,8 +16,14 @@ namespace HotChocolate.Data.Sorting;
1016
/// </summary>
1117
public class SortingContext : ISortingContext
1218
{
19+
private static readonly MethodInfo _createSortByMethod =
20+
typeof(SortingContext).GetMethod(nameof(CreateSortBy), BindingFlags.NonPublic | BindingFlags.Static)!;
21+
private static readonly ConcurrentDictionary<(Type, Type), MethodInfo> _sortByFactoryCache = new();
22+
private static readonly SortDefinitionFormatter _formatter = new();
1323
private readonly IReadOnlyList<SortingInfo> _value;
1424
private readonly IResolverContext _context;
25+
private readonly IType _type;
26+
private readonly IValueNode _valueNode;
1527

1628
/// <summary>
1729
/// Creates a new instance of <see cref="SortingContext" />
@@ -28,6 +40,8 @@ public SortingContext(
2840
.ToArray()
2941
: [new SortingInfo(type, valueNode, inputParser),];
3042
_context = context;
43+
_type = type;
44+
_valueNode = valueNode;
3145
}
3246

3347
/// <inheritdoc />
@@ -102,8 +116,97 @@ void SerializeAndAssign(string fieldName, ISortingValueNode? value)
102116
}
103117
}
104118

105-
public SortDefinition<T> AsSortDefinition<T>()
119+
public SortDefinition<T>? AsSortDefinition<T>()
106120
{
107-
throw new NotImplementedException();
121+
if(_valueNode.Kind == SyntaxKind.NullValue
122+
|| (_valueNode is ListValueNode listValue && listValue.Items.Count == 0)
123+
|| (_valueNode is ObjectValueNode objectValue && objectValue.Fields.Count == 0))
124+
{
125+
return null;
126+
}
127+
128+
var builder = ImmutableArray.CreateBuilder<ISortBy<T>>();
129+
var parameter = Expression.Parameter(typeof(T), "t");
130+
131+
foreach (var (selector, ascending, type) in _formatter.Rewrite<T>(_valueNode, _type, parameter))
132+
{
133+
var factory = _sortByFactoryCache.GetOrAdd(
134+
(typeof(T), type),
135+
static key => _createSortByMethod.MakeGenericMethod(key.Item1, key.Item2));
136+
var sortBy = (ISortBy<T>)factory.Invoke(null, [parameter, selector, ascending])!;
137+
builder.Add(sortBy);
138+
}
139+
140+
return new SortDefinition<T>(builder.ToImmutable());
141+
}
142+
143+
private static SortBy<TEntity, TValue> CreateSortBy<TEntity, TValue>(
144+
ParameterExpression parameter,
145+
Expression selector,
146+
bool ascending)
147+
=> new SortBy<TEntity, TValue>(
148+
Expression.Lambda<Func<TEntity, TValue>>(selector, parameter),
149+
ascending);
150+
151+
private sealed class SortDefinitionFormatter : SyntaxWalker<SortDefinitionFormatter.Context>
152+
{
153+
public IEnumerable<(Expression, bool, Type)> Rewrite<T>(IValueNode node, IType type, Expression parameter)
154+
{
155+
var context = new Context();
156+
context.Types.Push((InputObjectType)type);
157+
context.Parents.Push(parameter);
158+
Visit(node, context);
159+
return context.Completed;
160+
}
161+
162+
protected override ISyntaxVisitorAction Enter(
163+
ObjectFieldNode node,
164+
Context context)
165+
{
166+
var type = context.Types.Peek();
167+
var field = (SortField)type.Fields[node.Name.Value];
168+
var fieldType = field.Type.NamedType();
169+
170+
var parent = context.Parents.Peek();
171+
context.Parents.Push(Expression.Property(parent, (PropertyInfo)field.Member!));
172+
173+
if (fieldType.IsInputObjectType())
174+
{
175+
context.Types.Push((InputObjectType)fieldType);
176+
}
177+
178+
return base.Leave(node, context);
179+
}
180+
181+
protected override ISyntaxVisitorAction Leave(
182+
ObjectFieldNode node,
183+
Context context)
184+
{
185+
var type = context.Types.Peek();
186+
var field = (SortField)type.Fields[node.Name.Value];
187+
var fieldType = field.Type.NamedType();
188+
var expression = context.Parents.Pop();
189+
190+
if (fieldType.IsInputObjectType())
191+
{
192+
context.Types.Pop();
193+
}
194+
else
195+
{
196+
var ascending = node.Value.Value?.Equals("ASC") ?? true;
197+
context.Completed.Add((expression, ascending, field.Member!.GetReturnType()));
198+
}
199+
200+
return base.Leave(node, context);
201+
}
202+
203+
public class Context
204+
{
205+
public Stack<InputObjectType> Types { get; } = new();
206+
207+
public Stack<Expression> Parents { get; } = new();
208+
209+
public List<(Expression, bool, Type)> Completed { get; } = new();
210+
}
108211
}
109212
}

src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// ReSharper disable MoveLocalFunctionAfterJumpStatement
66

77
using HotChocolate.Data.Filters;
8+
using HotChocolate.Data.Sorting;
89
using HotChocolate.Execution;
910
using HotChocolate.Types;
1011
using Microsoft.Extensions.DependencyInjection;
@@ -879,6 +880,58 @@ public async Task AsPredicate_With_Filter_Returns_Author_1()
879880
result.MatchSnapshot();
880881
}
881882

883+
[Fact]
884+
public async Task AsSortDefinition_Descending()
885+
{
886+
// arrange
887+
var executor = await new ServiceCollection()
888+
.AddGraphQL()
889+
.AddFiltering()
890+
.AddSorting()
891+
.AddProjections()
892+
.AddQueryType<AsPredicateQuery>()
893+
.BuildRequestExecutorAsync();
894+
895+
// act
896+
var result = await executor.ExecuteAsync(
897+
"""
898+
{
899+
authorsSorted(order: { name: DESC }) {
900+
name
901+
}
902+
}
903+
""");
904+
905+
// assert
906+
result.MatchSnapshot();
907+
}
908+
909+
[Fact]
910+
public async Task AsSortDefinition_Descending_DataContext()
911+
{
912+
// arrange
913+
var executor = await new ServiceCollection()
914+
.AddGraphQL()
915+
.AddFiltering()
916+
.AddSorting()
917+
.AddProjections()
918+
.AddQueryType<AsPredicateQuery>()
919+
.BuildRequestExecutorAsync();
920+
921+
// act
922+
var result = await executor.ExecuteAsync(
923+
"""
924+
{
925+
authorsData(order: { name: DESC }) {
926+
name
927+
}
928+
}
929+
""");
930+
931+
// assert
932+
result.MatchSnapshot();
933+
}
934+
882935
[QueryType]
883936
public static class StaticQuery
884937
{
@@ -1082,5 +1135,39 @@ public IQueryable<Author> GetAuthors(IFilterContext filter)
10821135
},
10831136
}.AsQueryable()
10841137
.Where(filter);
1138+
1139+
[UseSorting]
1140+
public IQueryable<Author> GetAuthorsSorted(ISortingContext sorting)
1141+
=> new[]
1142+
{
1143+
new Author
1144+
{
1145+
Name = "Author1",
1146+
Books = new List<Book>(),
1147+
},
1148+
new Author
1149+
{
1150+
Name = "Author2",
1151+
Books = new List<Book>()
1152+
},
1153+
}.AsQueryable()
1154+
.Order(sorting);
1155+
1156+
[UseSorting]
1157+
public IQueryable<Author> GetAuthorsData(DataContext<Author> context)
1158+
=> new[]
1159+
{
1160+
new Author
1161+
{
1162+
Name = "Author1",
1163+
Books = new List<Book>(),
1164+
},
1165+
new Author
1166+
{
1167+
Name = "Author2",
1168+
Books = new List<Book>()
1169+
},
1170+
}.AsQueryable()
1171+
.Apply(context);
10851172
}
10861173
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"data": {
3+
"authorsSorted": [
4+
{
5+
"name": "Author2"
6+
},
7+
{
8+
"name": "Author1"
9+
}
10+
]
11+
}
12+
}

0 commit comments

Comments
 (0)