Skip to content

Commit 46f5a78

Browse files
committed
Fixed issues with ToPageAsync() and relative cursors. (#8110)
1 parent ba9b6c4 commit 46f5a78

File tree

45 files changed

+1717
-3572
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1717
-3572
lines changed

src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs

+76-138
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal static class ExpressionHelpers
4040
/// <exception cref="ArgumentException">
4141
/// If the number of keys does not match the number of values.
4242
/// </exception>
43-
public static (Expression<Func<T, bool>> WhereExpression, int Offset, bool ReverseOrder) BuildWhereExpression<T>(
43+
public static (Expression<Func<T, bool>> WhereExpression, int Offset) BuildWhereExpression<T>(
4444
ReadOnlySpan<CursorKey> keys,
4545
Cursor cursor,
4646
bool forward)
@@ -77,53 +77,34 @@ public static (Expression<Func<T, bool>> WhereExpression, int Offset, bool Rever
7777
{
7878
var handledKey = handled[j];
7979

80-
keyExpr =
81-
Expression.Equal(
82-
Expression.Call(
83-
ReplaceParameter(handledKey.Expression, parameter),
84-
handledKey.CompareMethod,
85-
cursorExpr[j]),
86-
zero);
87-
88-
current = current is null
89-
? keyExpr
90-
: Expression.AndAlso(current, keyExpr);
80+
keyExpr = Expression.Equal(
81+
Expression.Call(ReplaceParameter(handledKey.Expression, parameter), handledKey.CompareMethod,
82+
cursorExpr[j]), zero);
83+
84+
current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr);
9185
}
9286

9387
var greaterThan = forward
94-
? key.Direction is CursorKeyDirection.Ascending
95-
: key.Direction is CursorKeyDirection.Descending;
96-
97-
keyExpr =
98-
greaterThan
99-
? Expression.GreaterThan(
100-
Expression.Call(
101-
ReplaceParameter(key.Expression, parameter),
102-
key.CompareMethod,
103-
cursorExpr[i]),
104-
zero)
105-
: Expression.LessThan(
106-
Expression.Call(
107-
ReplaceParameter(key.Expression, parameter),
108-
key.CompareMethod,
109-
cursorExpr[i]),
110-
zero);
111-
112-
current = current is null
113-
? keyExpr
114-
: Expression.AndAlso(current, keyExpr);
115-
expression = expression is null
116-
? current
117-
: Expression.OrElse(expression, current);
88+
? key.Direction == CursorKeyDirection.Ascending
89+
: key.Direction == CursorKeyDirection.Descending;
90+
91+
keyExpr = greaterThan
92+
? Expression.GreaterThan(
93+
Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]),
94+
zero)
95+
: Expression.LessThan(
96+
Expression.Call(ReplaceParameter(key.Expression, parameter), key.CompareMethod, cursorExpr[i]),
97+
zero);
98+
99+
current = current is null ? keyExpr : Expression.AndAlso(current, keyExpr);
100+
expression = expression is null ? current : Expression.OrElse(expression, current);
118101
handled.Add(key);
119102
}
120103

121-
var offset = cursor.Offset ?? 0;
122-
var reverseOrder = offset < 0; // Reverse order if offset is negative
123-
124-
return (Expression.Lambda<Func<T, bool>>(expression!, parameter), Math.Abs(offset), reverseOrder);
104+
return (Expression.Lambda<Func<T, bool>>(expression!, parameter), cursor.Offset ?? 0);
125105
}
126106

107+
127108
/// <summary>
128109
/// Build the select expression for a batch paging expression that uses grouping.
129110
/// </summary>
@@ -151,12 +132,11 @@ public static (Expression<Func<T, bool>> WhereExpression, int Offset, bool Rever
151132
/// <typeparam name="TV">
152133
/// The value type.
153134
/// </typeparam>
154-
/// <returns></returns>
155135
/// <exception cref="ArgumentException">
156136
/// If the number of keys is less than one or
157137
/// the number of order expressions does not match the number of order methods.
158138
/// </exception>
159-
public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpression, bool ReverseOrder) BuildBatchSelectExpression<TK, TV>(
139+
public static BatchExpression<TK, TV> BuildBatchExpression<TK, TV>(
160140
PagingArguments arguments,
161141
ReadOnlySpan<CursorKey> keys,
162142
ReadOnlySpan<LambdaExpression> orderExpressions,
@@ -187,7 +167,8 @@ public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpressi
187167
var methodName = forward ? orderMethods[i] : ReverseOrder(orderMethods[i]);
188168
var orderExpression = orderExpressions[i];
189169
var delegateType = typeof(Func<,>).MakeGenericType(typeof(TV), orderExpression.Body.Type);
190-
var typedOrderExpression = Expression.Lambda(delegateType, orderExpression.Body, orderExpression.Parameters);
170+
var typedOrderExpression =
171+
Expression.Lambda(delegateType, orderExpression.Body, orderExpression.Parameters);
191172

192173
var method = GetEnumerableMethod(methodName, typeof(TV), typedOrderExpression);
193174

@@ -197,44 +178,68 @@ public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpressi
197178
typedOrderExpression);
198179
}
199180

200-
var reverseOrder = false;
201181
var offset = 0;
182+
var usesRelativeCursors = false;
183+
Cursor? cursor = null;
202184

203185
if (arguments.After is not null)
204186
{
205-
var cursor = CursorParser.Parse(arguments.After, keys);
206-
var (whereExpr, cursorOffset, reverse) = BuildWhereExpression<TV>(keys, cursor, forward: true);
187+
cursor = CursorParser.Parse(arguments.After, keys);
188+
var (whereExpr, cursorOffset) = BuildWhereExpression<TV>(keys, cursor, forward: true);
207189
source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr);
208190
offset = cursorOffset;
209-
reverseOrder = reverse;
191+
192+
if (cursor.IsRelative)
193+
{
194+
usesRelativeCursors = true;
195+
}
210196
}
211197

212198
if (arguments.Before is not null)
213199
{
214-
var cursor = CursorParser.Parse(arguments.Before, keys);
215-
var (whereExpr, cursorOffset, reverse) = BuildWhereExpression<TV>(keys, cursor, forward: false);
200+
if (usesRelativeCursors)
201+
{
202+
throw new ArgumentException(
203+
"You cannot use `before` and `after` with relative cursors at the same time.",
204+
nameof(arguments));
205+
}
206+
207+
cursor = CursorParser.Parse(arguments.Before, keys);
208+
var (whereExpr, cursorOffset) = BuildWhereExpression<TV>(keys, cursor, forward: false);
216209
source = Expression.Call(typeof(Enumerable), "Where", [typeof(TV)], source, whereExpr);
217210
offset = cursorOffset;
218-
reverseOrder = reverse;
219211
}
220212

221-
if (reverseOrder)
213+
if (arguments.First is not null)
222214
{
223-
source = Expression.Call(
224-
typeof(Enumerable),
225-
"Reverse",
226-
[typeof(TV)],
227-
source);
215+
requestedCount = arguments.First.Value;
216+
}
217+
218+
if (arguments.Last is not null)
219+
{
220+
requestedCount = arguments.Last.Value;
221+
}
222+
223+
if (arguments.EnableRelativeCursors && cursor?.IsRelative == true)
224+
{
225+
if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0))
226+
{
227+
throw new ArgumentException(
228+
"Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.",
229+
nameof(arguments));
230+
}
228231
}
229232

233+
offset = Math.Abs(offset);
234+
230235
if (offset > 0)
231236
{
232237
source = Expression.Call(
233238
typeof(Enumerable),
234239
"Skip",
235240
[typeof(TV)],
236241
source,
237-
Expression.Constant(offset));
242+
Expression.Constant(offset * requestedCount));
238243
}
239244

240245
if (arguments.First is not null)
@@ -245,7 +250,6 @@ public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpressi
245250
[typeof(TV)],
246251
source,
247252
Expression.Constant(arguments.First.Value + 1));
248-
requestedCount = arguments.First.Value;
249253
}
250254

251255
if (arguments.Last is not null)
@@ -256,7 +260,6 @@ public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpressi
256260
[typeof(TV)],
257261
source,
258262
Expression.Constant(arguments.Last.Value + 1));
259-
requestedCount = arguments.Last.Value;
260263
}
261264

262265
source = Expression.Call(
@@ -273,7 +276,10 @@ public static (Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpressi
273276
};
274277

275278
var createGroup = Expression.MemberInit(Expression.New(groupType), bindings);
276-
return (Expression.Lambda<Func<IGrouping<TK, TV>, Group<TK, TV>>>(createGroup, group), reverseOrder);
279+
return new BatchExpression<TK, TV>(
280+
Expression.Lambda<Func<IGrouping<TK, TV>, Group<TK, TV>>>(createGroup, group),
281+
arguments.Last is not null,
282+
cursor);
277283

278284
static string ReverseOrder(string method)
279285
=> method switch
@@ -292,84 +298,6 @@ static MethodInfo GetEnumerableMethod(string methodName, Type elementType, Lambd
292298
.MakeGenericMethod(elementType, keySelector.Body.Type);
293299
}
294300

295-
private static MethodCallExpression BuildBatchWhereExpression<T>(
296-
Expression enumerable,
297-
ReadOnlySpan<CursorKey> keys,
298-
object?[] cursor,
299-
bool forward)
300-
{
301-
var cursorExpr = new Expression[cursor.Length];
302-
303-
for (var i = 0; i < cursor.Length; i++)
304-
{
305-
cursorExpr[i] = CreateParameter(cursor[i], keys[i].Expression.ReturnType);
306-
}
307-
308-
var handled = new List<CursorKey>();
309-
Expression? expression = null;
310-
311-
var parameter = Expression.Parameter(typeof(T), "t");
312-
var zero = Expression.Constant(0);
313-
314-
for (var i = 0; i < keys.Length; i++)
315-
{
316-
var key = keys[i];
317-
Expression? current = null;
318-
Expression keyExpr;
319-
320-
for (var j = 0; j < handled.Count; j++)
321-
{
322-
var handledKey = handled[j];
323-
324-
keyExpr =
325-
Expression.Equal(
326-
Expression.Call(
327-
ReplaceParameter(handledKey.Expression, parameter),
328-
handledKey.CompareMethod,
329-
cursorExpr[j]),
330-
zero);
331-
332-
current = current is null
333-
? keyExpr
334-
: Expression.AndAlso(current, keyExpr);
335-
}
336-
337-
var greaterThan = forward
338-
? key.Direction is CursorKeyDirection.Ascending
339-
: key.Direction is CursorKeyDirection.Descending;
340-
341-
keyExpr =
342-
greaterThan
343-
? Expression.GreaterThan(
344-
Expression.Call(
345-
ReplaceParameter(key.Expression, parameter),
346-
key.CompareMethod,
347-
cursorExpr[i]),
348-
zero)
349-
: Expression.LessThan(
350-
Expression.Call(
351-
ReplaceParameter(key.Expression, parameter),
352-
key.CompareMethod,
353-
cursorExpr[i]),
354-
zero);
355-
356-
current = current is null
357-
? keyExpr
358-
: Expression.AndAlso(current, keyExpr);
359-
expression = expression is null
360-
? current
361-
: Expression.OrElse(expression, current);
362-
handled.Add(key);
363-
}
364-
365-
return Expression.Call(
366-
typeof(Enumerable),
367-
"Where",
368-
[typeof(T)],
369-
enumerable,
370-
Expression.Lambda<Func<T, bool>>(expression!, parameter));
371-
}
372-
373301
/// <summary>
374302
/// Extracts and removes the orderBy and thenBy expressions from the given expression tree.
375303
/// </summary>
@@ -504,4 +432,14 @@ private static Expression StripQuotes(Expression e)
504432
return e;
505433
}
506434
}
435+
436+
internal readonly struct BatchExpression<TK, TV>(
437+
Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> selectExpression,
438+
bool isBackward,
439+
Cursor? cursor)
440+
{
441+
public Expression<Func<IGrouping<TK, TV>, Group<TK, TV>>> SelectExpression { get; } = selectExpression;
442+
public bool IsBackward { get; } = isBackward;
443+
public Cursor? Cursor { get; } = cursor;
444+
}
507445
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
4+
namespace GreenDonut.Data.Expressions;
5+
6+
public class ReverseOrderExpressionRewriter : ExpressionVisitor
7+
{
8+
private static readonly MethodInfo _orderByMethod = typeof(Queryable).GetMethods()
9+
.First(m => m.Name == nameof(Queryable.OrderBy) && m.GetParameters().Length == 2);
10+
11+
private static readonly MethodInfo _orderByDescendingMethod = typeof(Queryable).GetMethods()
12+
.First(m => m.Name == nameof(Queryable.OrderByDescending) && m.GetParameters().Length == 2);
13+
14+
private static readonly MethodInfo _thenByMethod = typeof(Queryable).GetMethods()
15+
.First(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);
16+
17+
private static readonly MethodInfo _thenByDescendingMethod = typeof(Queryable).GetMethods()
18+
.First(m => m.Name == nameof(Queryable.ThenByDescending) && m.GetParameters().Length == 2);
19+
20+
protected override Expression VisitMethodCall(MethodCallExpression node)
21+
{
22+
var visitedArguments = node.Arguments.Select(Visit).Cast<Expression>().ToArray();
23+
24+
if (node.Method.Name == nameof(Queryable.OrderBy))
25+
{
26+
return Expression.Call(
27+
_orderByDescendingMethod.MakeGenericMethod(node.Method.GetGenericArguments()),
28+
visitedArguments);
29+
}
30+
31+
if (node.Method.Name == nameof(Queryable.OrderByDescending))
32+
{
33+
return Expression.Call(
34+
_orderByMethod.MakeGenericMethod(node.Method.GetGenericArguments()),
35+
visitedArguments);
36+
}
37+
38+
if (node.Method.Name == nameof(Queryable.ThenBy))
39+
{
40+
return Expression.Call(
41+
_thenByDescendingMethod.MakeGenericMethod(node.Method.GetGenericArguments()),
42+
visitedArguments);
43+
}
44+
45+
if (node.Method.Name == nameof(Queryable.ThenByDescending))
46+
{
47+
return Expression.Call(
48+
_thenByMethod.MakeGenericMethod(node.Method.GetGenericArguments()),
49+
visitedArguments);
50+
}
51+
52+
return base.VisitMethodCall(node);
53+
}
54+
55+
public static IQueryable<T> Rewrite<T>(IQueryable<T> query)
56+
{
57+
var reversedExpression = new ReverseOrderExpressionRewriter().Visit(query.Expression);
58+
return query.Provider.CreateQuery<T>(reversedExpression);
59+
}
60+
}

0 commit comments

Comments
 (0)