Skip to content

Commit 7b27488

Browse files
PascalSennmoconnell
andauthoredMay 31, 2021
Adds new pagination helper to main (#3733)
Co-authored-by: matthewoconnell <matthew.n.oconnell@gmail.com>
1 parent ff900c1 commit 7b27488

13 files changed

+1661
-249
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace HotChocolate.Types.Pagination
8+
{
9+
public static class CursorPagingHelper
10+
{
11+
public delegate ValueTask<IReadOnlyList<IndexEdge<TEntity>>>
12+
ToIndexEdgesAsync<TSource, TEntity>(
13+
TSource source,
14+
int offset,
15+
CancellationToken cancellationToken);
16+
17+
public delegate TSource ApplySkip<TSource>(TSource source, int skip);
18+
19+
public delegate TSource ApplyTake<TSource>(TSource source, int take);
20+
21+
public delegate ValueTask<int> CountAsync<TSource>(
22+
TSource source,
23+
CancellationToken cancellationToken);
24+
25+
public static async ValueTask<Connection> ApplyPagination<TSource, TEntity>(
26+
TSource source,
27+
CursorPagingArguments arguments,
28+
ApplySkip<TSource> applySkip,
29+
ApplyTake<TSource> applyTake,
30+
ToIndexEdgesAsync<TSource, TEntity> toIndexEdgesAsync,
31+
CountAsync<TSource> countAsync,
32+
CancellationToken cancellationToken = default)
33+
{
34+
// We only need the maximal element count if no `before` counter is set and no `first`
35+
// argument is provided.
36+
var maxElementCount = int.MaxValue;
37+
if (arguments.Before is null && arguments.First is null)
38+
{
39+
var count = await countAsync(source, cancellationToken);
40+
maxElementCount = count;
41+
42+
// in case we already know the total count, we override the countAsync parameter
43+
// so that we do not have to fetch the count twice
44+
countAsync = (_, _) => new ValueTask<int>(count);
45+
}
46+
47+
Range range = SliceRange<TEntity>(arguments, maxElementCount);
48+
49+
var skip = range.Start;
50+
var take = range.Count();
51+
52+
// we fetch one element more than we requested
53+
if (take != maxElementCount)
54+
{
55+
take++;
56+
}
57+
58+
TSource slicedSource = source;
59+
if (skip != 0)
60+
{
61+
slicedSource = applySkip(source, skip);
62+
}
63+
64+
if (take != maxElementCount)
65+
{
66+
slicedSource = applyTake(slicedSource, take);
67+
}
68+
69+
IReadOnlyList<IndexEdge<TEntity>> selectedEdges =
70+
await toIndexEdgesAsync(slicedSource, skip, cancellationToken);
71+
72+
bool moreItemsReturnedThanRequested = selectedEdges.Count > range.Count();
73+
bool isSequenceFromStart = range.Start == 0;
74+
75+
selectedEdges = new SkipLastCollection<IndexEdge<TEntity>>(
76+
selectedEdges,
77+
moreItemsReturnedThanRequested);
78+
79+
ConnectionPageInfo pageInfo =
80+
CreatePageInfo(isSequenceFromStart, moreItemsReturnedThanRequested, selectedEdges);
81+
82+
return new Connection<TEntity>(
83+
selectedEdges,
84+
pageInfo,
85+
async ct => await countAsync(source, ct));
86+
}
87+
88+
private static ConnectionPageInfo CreatePageInfo<TEntity>(
89+
bool isSequenceFromStart,
90+
bool moreItemsReturnedThanRequested,
91+
IReadOnlyList<IndexEdge<TEntity>> selectedEdges)
92+
{
93+
// We know that there is a next page if more items than requested are returned
94+
bool hasNextPage = moreItemsReturnedThanRequested;
95+
96+
// There is a previous page if the sequence start is not 0.
97+
// If you point to index 2 of a empty list, we assume that there is a previous page
98+
bool hasPreviousPage = !isSequenceFromStart;
99+
100+
IndexEdge<TEntity>? firstEdge = null;
101+
IndexEdge<TEntity>? lastEdge = null;
102+
103+
if (selectedEdges.Count > 0)
104+
{
105+
firstEdge = selectedEdges[0];
106+
lastEdge = selectedEdges[selectedEdges.Count - 1];
107+
}
108+
109+
return new ConnectionPageInfo(
110+
hasNextPage,
111+
hasPreviousPage,
112+
firstEdge?.Cursor,
113+
lastEdge?.Cursor);
114+
}
115+
116+
private static Range SliceRange<TEntity>(
117+
CursorPagingArguments arguments,
118+
int maxElementCount)
119+
{
120+
// [SPEC] if after is set then remove all elements of edges before and including
121+
// afterEdge.
122+
//
123+
// The cursor is increased by one so that the index points to the element after
124+
var startIndex = arguments.After is { } a
125+
? IndexEdge<TEntity>.DeserializeCursor(a) + 1
126+
: 0;
127+
128+
// [SPEC] if before is set then remove all elements of edges before and including
129+
// beforeEdge.
130+
int before = arguments.Before is { } b
131+
? IndexEdge<TEntity>.DeserializeCursor(b)
132+
: maxElementCount;
133+
134+
// if after is negative we have know how much of the offset was in the negative range.
135+
// The amount of positions that are in the negative range, have to be subtracted from
136+
// the take or we will fetch too many items.
137+
int startOffsetCorrection = 0;
138+
if (startIndex < 0)
139+
{
140+
startOffsetCorrection = Math.Abs(startIndex);
141+
startIndex = 0;
142+
}
143+
144+
Range range = new(startIndex, before);
145+
146+
//[SPEC] If first is less than 0 throw an error
147+
ValidateFirst(arguments, out int? first);
148+
if (first is { })
149+
{
150+
first -= startOffsetCorrection;
151+
if (first < 0)
152+
{
153+
first = 0;
154+
}
155+
}
156+
157+
//[SPEC] Slice edges to be of length first by removing edges from the end of edges.
158+
range.Take(first);
159+
160+
//[SPEC] if last is less than 0 throw an error
161+
ValidateLast(arguments, out int? last);
162+
//[SPEC] Slice edges to be of length last by removing edges from the start of edges.
163+
range.TakeLast(last);
164+
165+
return range;
166+
}
167+
168+
private class SkipLastCollection<T> : IReadOnlyList<T>
169+
{
170+
private readonly IReadOnlyList<T> _items;
171+
private readonly bool _skipLast;
172+
173+
public SkipLastCollection(
174+
IReadOnlyList<T> items,
175+
bool skipLast = false)
176+
{
177+
_items = items;
178+
_skipLast = skipLast;
179+
Count = _items.Count;
180+
181+
if (skipLast && Count > 0)
182+
{
183+
Count--;
184+
}
185+
}
186+
187+
public int Count { get; }
188+
189+
public IEnumerator<T> GetEnumerator()
190+
{
191+
for (var i = 0; i < _items.Count; i++)
192+
{
193+
if (i == _items.Count - 1 && _skipLast)
194+
{
195+
break;
196+
}
197+
198+
yield return _items[i];
199+
}
200+
}
201+
202+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
203+
204+
public T this[int index] => _items[index];
205+
}
206+
207+
internal class Range
208+
{
209+
public Range(int start, int end)
210+
{
211+
Start = start;
212+
End = end;
213+
}
214+
215+
public int Start { get; private set; }
216+
217+
public int End { get; private set; }
218+
219+
public int Count()
220+
{
221+
if (End < Start)
222+
{
223+
return 0;
224+
}
225+
226+
return End - Start;
227+
}
228+
229+
public void Take(int? first)
230+
{
231+
if (first is { })
232+
{
233+
var end = Start + first.Value;
234+
if (End > end)
235+
{
236+
End = end;
237+
}
238+
}
239+
}
240+
241+
public void TakeLast(int? last)
242+
{
243+
if (last is { })
244+
{
245+
var start = End - last.Value;
246+
if (Start < start)
247+
{
248+
Start = start;
249+
}
250+
}
251+
}
252+
}
253+
254+
private static void ValidateFirst(
255+
CursorPagingArguments arguments,
256+
out int? first)
257+
{
258+
if (arguments.First < 0)
259+
{
260+
throw new ArgumentOutOfRangeException(nameof(first));
261+
}
262+
263+
first = arguments.First;
264+
}
265+
266+
private static void ValidateLast(
267+
CursorPagingArguments arguments,
268+
out int? last)
269+
{
270+
if (arguments.Last < 0)
271+
{
272+
throw new ArgumentOutOfRangeException(nameof(last));
273+
}
274+
275+
last = arguments.Last;
276+
}
277+
}
278+
}
279+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using HotChocolate.Types.Pagination;
6+
7+
namespace HotChocolate.Types
8+
{
9+
public static class EnumerableCursorPagingExtensions
10+
{
11+
public static ValueTask<Connection> ApplyCursorPaginationAsync<TSource>(
12+
this IEnumerable<TSource> source,
13+
int? first = null,
14+
int? last = null,
15+
string? after = null,
16+
string? before = null,
17+
CancellationToken cancellationToken = default) =>
18+
CursorPagingHelper.ApplyPagination(
19+
source,
20+
new CursorPagingArguments(first, last, after, before),
21+
(x, skip) => x.Skip(skip),
22+
(x, take) => x.Take(take),
23+
Execute,
24+
CountAsync,
25+
cancellationToken);
26+
27+
public static ValueTask<Connection> ApplyCursorPaginationAsync<TSource>(
28+
this IEnumerable<TSource> source,
29+
CursorPagingArguments arguments,
30+
CancellationToken cancellationToken = default) =>
31+
CursorPagingHelper.ApplyPagination(
32+
source,
33+
arguments,
34+
(x, skip) => x.Skip(skip),
35+
(x, take) => x.Take(take),
36+
Execute,
37+
CountAsync,
38+
cancellationToken);
39+
40+
private static async ValueTask<int> CountAsync<TEntity>(
41+
IEnumerable<TEntity> source,
42+
CancellationToken cancellationToken) =>
43+
await Task.Run(source.Count, cancellationToken).ConfigureAwait(false);
44+
45+
private static async ValueTask<IReadOnlyList<IndexEdge<TEntity>>> Execute<TEntity>(
46+
IEnumerable<TEntity> queryable,
47+
int offset,
48+
CancellationToken cancellationToken)
49+
{
50+
var list = new List<IndexEdge<TEntity>>();
51+
52+
if (queryable is IAsyncEnumerable<TEntity> enumerable)
53+
{
54+
var index = offset;
55+
await foreach (TEntity item in enumerable.WithCancellation(cancellationToken)
56+
.ConfigureAwait(false))
57+
{
58+
list.Add(IndexEdge<TEntity>.Create(item, index++));
59+
}
60+
}
61+
else
62+
{
63+
await Task.Run(() =>
64+
{
65+
var index = offset;
66+
foreach (TEntity item in queryable)
67+
{
68+
if (cancellationToken.IsCancellationRequested)
69+
{
70+
break;
71+
}
72+
73+
list.Add(IndexEdge<TEntity>.Create(item, index++));
74+
}
75+
},
76+
cancellationToken)
77+
.ConfigureAwait(false);
78+
}
79+
80+
return list;
81+
}
82+
}
83+
}

‎src/HotChocolate/Core/src/Types.CursorPagination/HotChocolate.Types.CursorPagination.csproj

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,5 @@
3737
<AutoGen>True</AutoGen>
3838
<DependentUpon>CursorResources.resx</DependentUpon>
3939
</Compile>
40-
</ItemGroup>
41-
40+
</ItemGroup>
4241
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("HotChocolate.Types.Tests")]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using HotChocolate.Types.Pagination;
6+
7+
namespace HotChocolate.Types
8+
{
9+
public static class EnumerableOffsetPagingExtensions
10+
{
11+
public static ValueTask<CollectionSegment> ApplyOffsetPaginationAsync<TItemType>(
12+
this IEnumerable<TItemType> source,
13+
int? skip = null,
14+
int? take = null,
15+
CancellationToken cancellationToken = default) =>
16+
OffsetPagingHelper.ApplyPagination(
17+
source,
18+
new OffsetPagingArguments(skip, take ?? 0),
19+
(x, s) => x.Skip(s),
20+
(x, t) => x.Take(t),
21+
OffsetPagingHelper.ExecuteEnumerable,
22+
CountAsync,
23+
cancellationToken);
24+
25+
public static ValueTask<CollectionSegment> ApplyOffsetPaginationAsync<TItemType>(
26+
this IEnumerable<TItemType> source,
27+
OffsetPagingArguments arguments,
28+
CancellationToken cancellationToken = default) =>
29+
OffsetPagingHelper.ApplyPagination(
30+
source,
31+
arguments,
32+
(x, skip) => x.Skip(skip),
33+
(x, take) => x.Take(take),
34+
OffsetPagingHelper.ExecuteEnumerable,
35+
CountAsync,
36+
cancellationToken);
37+
38+
private static async ValueTask<int> CountAsync<TEntity>(
39+
IEnumerable<TEntity> source,
40+
CancellationToken cancellationToken) =>
41+
await Task.Run(source.Count, cancellationToken).ConfigureAwait(false);
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace HotChocolate.Types.Pagination
7+
{
8+
public static class OffsetPagingHelper
9+
{
10+
public delegate ValueTask<IReadOnlyList<TEntity>>
11+
Execute<TSource, TEntity>(
12+
TSource source,
13+
CancellationToken cancellationToken);
14+
15+
public delegate TSource ApplySkip<TSource>(TSource source, int skip);
16+
17+
public delegate TSource ApplyTake<TSource>(TSource source, int take);
18+
19+
public delegate ValueTask<int> CountAsync<TSource>(
20+
TSource source,
21+
CancellationToken cancellationToken);
22+
23+
public static async ValueTask<CollectionSegment> ApplyPagination<TSource, TEntity>(
24+
TSource source,
25+
OffsetPagingArguments arguments,
26+
ApplySkip<TSource> applySkip,
27+
ApplyTake<TSource> applyTake,
28+
Execute<TSource, TEntity> execute,
29+
CountAsync<TSource> countAsync,
30+
CancellationToken cancellationToken = default)
31+
{
32+
TSource sliced = source;
33+
34+
if (arguments.Skip is {} skip)
35+
{
36+
sliced = applySkip(sliced, skip);
37+
}
38+
39+
if (arguments.Take is { } take)
40+
{
41+
sliced = applyTake(sliced, take + 1);
42+
}
43+
44+
IReadOnlyList<TEntity> items =
45+
await execute(sliced, cancellationToken).ConfigureAwait(false);
46+
47+
bool hasNextPage = items.Count == arguments.Take + 1;
48+
bool hasPreviousPage = (arguments.Skip ?? 0) > 0;
49+
50+
CollectionSegmentInfo pageInfo = new(hasNextPage, hasPreviousPage);
51+
52+
items = new SkipLastCollection<TEntity>(items, skipLast: hasNextPage);
53+
54+
return new CollectionSegment(
55+
(IReadOnlyCollection<object>)items,
56+
pageInfo,
57+
async ct => await countAsync(source, ct));
58+
}
59+
60+
private class SkipLastCollection<T> : IReadOnlyList<T>
61+
{
62+
private readonly IReadOnlyList<T> _items;
63+
private readonly bool _skipLast;
64+
65+
public SkipLastCollection(
66+
IReadOnlyList<T> items,
67+
bool skipLast = false)
68+
{
69+
_items = items;
70+
_skipLast = skipLast;
71+
Count = _items.Count;
72+
73+
if (skipLast && Count > 0)
74+
{
75+
Count--;
76+
}
77+
}
78+
79+
public int Count { get; }
80+
81+
public IEnumerator<T> GetEnumerator()
82+
{
83+
for (var i = 0; i < _items.Count; i++)
84+
{
85+
if (i == _items.Count - 1 && _skipLast)
86+
{
87+
break;
88+
}
89+
90+
yield return _items[i];
91+
}
92+
}
93+
94+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
95+
96+
public T this[int index] => _items[index];
97+
}
98+
99+
internal static async ValueTask<IReadOnlyList<TItemType>> ExecuteEnumerable<TItemType>(
100+
IEnumerable<TItemType> queryable,
101+
CancellationToken cancellationToken)
102+
{
103+
var list = new List<TItemType>();
104+
105+
if (queryable is IAsyncEnumerable<TItemType> enumerable)
106+
{
107+
await foreach (TItemType item in enumerable.WithCancellation(cancellationToken)
108+
.ConfigureAwait(false))
109+
{
110+
list.Add(item);
111+
}
112+
}
113+
else
114+
{
115+
await Task.Run(() =>
116+
{
117+
foreach (TItemType item in queryable)
118+
{
119+
if (cancellationToken.IsCancellationRequested)
120+
{
121+
break;
122+
}
123+
124+
list.Add(item);
125+
}
126+
127+
}).ConfigureAwait(false);
128+
}
129+
130+
return list;
131+
}
132+
}
133+
}

‎src/HotChocolate/Core/test/Types.CursorPagination.Tests/CursorPagingHandlerTests.cs

+948
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace HotChocolate.Types.Pagination
9+
{
10+
public class EnumerableCursorPagingExtensionsTests
11+
{
12+
[Fact]
13+
public async Task ApplyPagination_After_2_First_2()
14+
{
15+
// arrange
16+
Foo[] data = Enumerable.Range(0, 10).Select(Foo.Create).ToArray();
17+
18+
// act
19+
Connection result =
20+
await data.ApplyCursorPaginationAsync(after: ToBase64(1), first: 2);
21+
22+
// assert
23+
Assert.Equal(2, ToFoo(result).First().Index);
24+
Assert.Equal(3, ToFoo(result).Last().Index);
25+
Assert.True(result.Info.HasNextPage);
26+
Assert.True(result.Info.HasPreviousPage);
27+
Assert.Equal(ToBase64(2), result.Info.StartCursor);
28+
Assert.Equal(ToBase64(3), result.Info.EndCursor);
29+
Assert.Equal(10, await result.GetTotalCountAsync(default));
30+
}
31+
32+
private static string ToBase64(int i) =>
33+
Convert.ToBase64String(Encoding.UTF8.GetBytes(i.ToString()));
34+
35+
private static IEnumerable<Foo> ToFoo(Connection connection)
36+
{
37+
return connection.Edges.Select(x => x.Node).OfType<Foo>();
38+
}
39+
40+
public class Foo
41+
{
42+
public Foo(int index)
43+
{
44+
Index = index;
45+
}
46+
47+
public int Index { get; }
48+
49+
public static Foo Create(int index) => new(index);
50+
}
51+
}
52+
}

‎src/HotChocolate/MongoDb/src/Data/Paging/AggregateFluentPagingContainer.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ public IMongoPagingContainer<TEntity> Take(int take)
6363
}
6464

6565
public static AggregateFluentPagingContainer<TEntity> New(
66-
IAggregateFluent<TEntity> aggregate) =>
67-
new AggregateFluentPagingContainer<TEntity>(aggregate);
66+
IAggregateFluent<TEntity> aggregate) => new(aggregate);
6867
}
6968
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Reflection;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -38,14 +39,17 @@ protected override CursorPagingHandler CreateHandler(
3839

3940
return (CursorPagingHandler)_createHandler
4041
.MakeGenericMethod(source.ElementType?.Source ?? source.Source)
41-
.Invoke(null, new object[] { options })!;
42+
.Invoke(null,
43+
new object[]
44+
{
45+
options
46+
})!;
4247
}
4348

4449
private static MongoDbCursorPagingHandler<TEntity> CreateHandlerInternal<TEntity>(
4550
PagingOptions options) =>
4651
new MongoDbCursorPagingHandler<TEntity>(options);
4752

48-
4953
private class MongoDbCursorPagingHandler<TEntity> : CursorPagingHandler
5054
{
5155
public MongoDbCursorPagingHandler(PagingOptions options) : base(options)
@@ -58,9 +62,39 @@ protected override ValueTask<Connection> SliceAsync(
5862
CursorPagingArguments arguments)
5963
{
6064
IMongoPagingContainer<TEntity> f = CreatePagingContainer(source);
61-
return ResolveAsync(f, arguments, context.RequestAborted);
65+
return CursorPagingHelper.ApplyPagination(
66+
f,
67+
arguments,
68+
ApplySkip,
69+
ApplyTake,
70+
ToIndexEdgesAsync,
71+
CountAsync,
72+
context.RequestAborted);
6273
}
6374

75+
private static ValueTask<IReadOnlyList<IndexEdge<TEntity>>>
76+
ToIndexEdgesAsync(
77+
IMongoPagingContainer<TEntity> source,
78+
int offset,
79+
CancellationToken cancellationToken)
80+
{
81+
return source.ToIndexEdgesAsync(offset, cancellationToken);
82+
}
83+
84+
private static IMongoPagingContainer<TEntity> ApplySkip(
85+
IMongoPagingContainer<TEntity> source,
86+
int skip) => source.Skip(skip);
87+
88+
89+
private static IMongoPagingContainer<TEntity> ApplyTake(
90+
IMongoPagingContainer<TEntity> source,
91+
int take) => source.Take(take);
92+
93+
private static async ValueTask<int> CountAsync(
94+
IMongoPagingContainer<TEntity> source,
95+
CancellationToken cancellationToken) =>
96+
await source.CountAsync(cancellationToken);
97+
6498
private IMongoPagingContainer<TEntity> CreatePagingContainer(object source)
6599
{
66100
return source switch
@@ -78,184 +112,6 @@ private IMongoPagingContainer<TEntity> CreatePagingContainer(object source)
78112
_ => throw ThrowHelper.PagingTypeNotSupported(source.GetType())
79113
};
80114
}
81-
82-
private async ValueTask<Connection> ResolveAsync(
83-
IMongoPagingContainer<TEntity> source,
84-
CursorPagingArguments arguments = default,
85-
CancellationToken cancellationToken = default)
86-
{
87-
var count = await source.CountAsync(cancellationToken).ConfigureAwait(false);
88-
89-
int? after = arguments.After is { } a
90-
? (int?)IndexEdge<TEntity>.DeserializeCursor(a)
91-
: null;
92-
93-
int? before = arguments.Before is { } b
94-
? (int?)IndexEdge<TEntity>.DeserializeCursor(b)
95-
: null;
96-
97-
IReadOnlyList<IndexEdge<TEntity>> selectedEdges =
98-
await GetSelectedEdgesAsync(
99-
source,
100-
arguments.First,
101-
arguments.Last,
102-
after,
103-
before,
104-
cancellationToken)
105-
.ConfigureAwait(false);
106-
107-
IndexEdge<TEntity>? firstEdge = selectedEdges.Count == 0
108-
? null
109-
: selectedEdges[0];
110-
111-
IndexEdge<TEntity>? lastEdge = selectedEdges.Count == 0
112-
? null
113-
: selectedEdges[selectedEdges.Count - 1];
114-
115-
var pageInfo = new ConnectionPageInfo(
116-
lastEdge?.Index < count - 1,
117-
firstEdge?.Index > 0,
118-
firstEdge?.Cursor,
119-
lastEdge?.Cursor,
120-
count);
121-
122-
return new Connection<TEntity>(
123-
selectedEdges,
124-
pageInfo,
125-
ct => new ValueTask<int>(pageInfo.TotalCount ?? 0));
126-
}
127-
128-
private async ValueTask<IReadOnlyList<IndexEdge<TEntity>>> GetSelectedEdgesAsync(
129-
IMongoPagingContainer<TEntity> allEdges,
130-
int? first,
131-
int? last,
132-
int? after,
133-
int? before,
134-
CancellationToken cancellationToken)
135-
{
136-
var (offset, edges) =
137-
await GetEdgesToReturnAsync(
138-
allEdges,
139-
first,
140-
last,
141-
after,
142-
before,
143-
cancellationToken)
144-
.ConfigureAwait(false);
145-
146-
return await ExecuteQueryableAsync(edges, offset, cancellationToken);
147-
}
148-
149-
private async Task<(int, IMongoPagingContainer<TEntity>)> GetEdgesToReturnAsync(
150-
IMongoPagingContainer<TEntity> allEdges,
151-
int? first,
152-
int? last,
153-
int? after,
154-
int? before,
155-
CancellationToken cancellationToken)
156-
{
157-
IMongoPagingContainer<TEntity> edges = ApplyCursorToEdges(allEdges, after, before);
158-
159-
var offset = 0;
160-
if (after.HasValue)
161-
{
162-
offset = after.Value + 1;
163-
}
164-
165-
if (first.HasValue)
166-
{
167-
edges = GetFirstEdges(edges, first.Value);
168-
}
169-
170-
if (last.HasValue)
171-
{
172-
var (newOffset, newEdges) =
173-
await GetLastEdgesAsync(edges, last.Value, offset, cancellationToken)
174-
.ConfigureAwait(false);
175-
176-
edges = newEdges;
177-
offset = newOffset;
178-
}
179-
180-
return (offset, edges);
181-
}
182-
183-
protected virtual IMongoPagingContainer<TEntity> GetFirstEdges(
184-
IMongoPagingContainer<TEntity> edges,
185-
int first)
186-
{
187-
if (first < 0)
188-
{
189-
throw new ArgumentOutOfRangeException(nameof(first));
190-
}
191-
192-
return edges.Take(first);
193-
}
194-
195-
protected virtual Task<(int, IMongoPagingContainer<TEntity>)> GetLastEdgesAsync(
196-
IMongoPagingContainer<TEntity> edges,
197-
int last,
198-
int offset,
199-
CancellationToken cancellationToken)
200-
{
201-
if (last < 0)
202-
{
203-
throw new ArgumentOutOfRangeException(nameof(last));
204-
}
205-
206-
return GetLastEdgesAsyncInternal(edges, last, offset, cancellationToken);
207-
}
208-
209-
private async Task<(int, IMongoPagingContainer<TEntity>)>
210-
GetLastEdgesAsyncInternal(
211-
IMongoPagingContainer<TEntity> edges,
212-
int last,
213-
int offset,
214-
CancellationToken cancellationToken)
215-
{
216-
IMongoPagingContainer<TEntity> temp = edges;
217-
218-
var count = await edges.CountAsync(cancellationToken).ConfigureAwait(false);
219-
var skip = count - last;
220-
221-
if (skip > 0)
222-
{
223-
temp = temp.Skip(skip);
224-
offset += count;
225-
offset -= await edges.CountAsync(cancellationToken).ConfigureAwait(false);
226-
}
227-
228-
return (offset, temp);
229-
}
230-
231-
protected virtual IMongoPagingContainer<TEntity> ApplyCursorToEdges(
232-
IMongoPagingContainer<TEntity> allEdges,
233-
int? after,
234-
int? before)
235-
{
236-
IMongoPagingContainer<TEntity> edges = allEdges;
237-
238-
if (after.HasValue)
239-
{
240-
edges = edges.Skip(after.Value + 1);
241-
}
242-
243-
if (before.HasValue)
244-
{
245-
edges = edges.Take(before.Value);
246-
}
247-
248-
return edges;
249-
}
250-
251-
protected virtual async ValueTask<IReadOnlyList<IndexEdge<TEntity>>>
252-
ExecuteQueryableAsync(
253-
IMongoPagingContainer<TEntity> container,
254-
int offset,
255-
CancellationToken cancellationToken)
256-
{
257-
return await container.ToIndexEdgesAsync(offset, cancellationToken);
258-
}
259115
}
260116
}
261117
}

‎src/HotChocolate/MongoDb/src/Data/Paging/MongoDbOffsetPagingProvider.cs

+24-64
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Threading.Tasks;
66
using HotChocolate.Internal;
77
using HotChocolate.Resolvers;
8-
using HotChocolate.Types;
98
using HotChocolate.Types.Pagination;
109
using MongoDB.Driver;
1110

@@ -46,8 +45,7 @@ protected override OffsetPagingHandler CreateHandler(
4645
}
4746

4847
private static MongoDbOffsetPagingHandler<TEntity> CreateHandlerInternal<TEntity>(
49-
PagingOptions options) => new MongoDbOffsetPagingHandler<TEntity>(options);
50-
48+
PagingOptions options) => new(options);
5149

5250
private class MongoDbOffsetPagingHandler<TEntity> : OffsetPagingHandler
5351
{
@@ -61,7 +59,14 @@ protected override ValueTask<CollectionSegment> SliceAsync(
6159
OffsetPagingArguments arguments)
6260
{
6361
IMongoPagingContainer<TEntity> f = CreatePagingContainer(source);
64-
return ResolveAsync(context, f, arguments);
62+
return OffsetPagingHelper.ApplyPagination(
63+
f,
64+
arguments,
65+
ApplySkip,
66+
ApplyTake,
67+
Execute,
68+
CountAsync,
69+
context.RequestAborted);
6570
}
6671

6772
private IMongoPagingContainer<TEntity> CreatePagingContainer(object source)
@@ -82,71 +87,26 @@ private IMongoPagingContainer<TEntity> CreatePagingContainer(object source)
8287
};
8388
}
8489

85-
private async ValueTask<CollectionSegment> ResolveAsync(
86-
IResolverContext context,
87-
IMongoPagingContainer<TEntity> queryable,
88-
OffsetPagingArguments arguments = default)
90+
private static async ValueTask<IReadOnlyList<TEntity>> Execute(
91+
IMongoPagingContainer<TEntity> source,
92+
CancellationToken cancellationToken)
8993
{
90-
IMongoPagingContainer<TEntity> original = queryable;
91-
92-
if (arguments.Skip.HasValue)
93-
{
94-
queryable = queryable.Skip(arguments.Skip.Value);
95-
}
96-
97-
queryable = queryable.Take(arguments.Take + 1);
98-
99-
List<TEntity> items = await queryable
100-
.ToListAsync(context.RequestAborted)
101-
.ConfigureAwait(false);
102-
103-
var pageInfo = new CollectionSegmentInfo(
104-
items.Count == arguments.Take + 1,
105-
(arguments.Skip ?? 0) > 0);
106-
107-
if (items.Count > arguments.Take)
108-
{
109-
items.RemoveAt(arguments.Take);
110-
}
111-
112-
Func<CancellationToken, ValueTask<int>> getTotalCount =
113-
ct => throw new InvalidOperationException();
94+
return await source.ToListAsync(cancellationToken);
95+
}
11496

115-
// TotalCount is one of the heaviest operations. It is only necessary to load totalCount
116-
// when it is enabled (IncludeTotalCount) and when it is contained in the selection set.
117-
if (IncludeTotalCount &&
118-
context.Field.Type is ObjectType objectType &&
119-
context.FieldSelection.SelectionSet is {} selectionSet)
120-
{
121-
IReadOnlyList<IFieldSelection> selections = context
122-
.GetSelections(objectType, selectionSet, true);
97+
private static IMongoPagingContainer<TEntity> ApplySkip(
98+
IMongoPagingContainer<TEntity> source,
99+
int skip) => source.Skip(skip);
123100

124-
var includeTotalCount = false;
125-
for (var i = 0; i < selections.Count; i++)
126-
{
127-
if (selections[i].Field.Name.Value is "totalCount")
128-
{
129-
includeTotalCount = true;
130-
break;
131-
}
132-
}
133101

134-
// When totalCount is included in the selection set we prefetch it, then capture the
135-
// count in a variable, to pass it into the clojure
136-
if (includeTotalCount)
137-
{
138-
var captureCount = await original
139-
.CountAsync(context.RequestAborted)
140-
.ConfigureAwait(false);
141-
getTotalCount = ct => new ValueTask<int>(captureCount);
142-
}
143-
}
102+
private static IMongoPagingContainer<TEntity> ApplyTake(
103+
IMongoPagingContainer<TEntity> source,
104+
int take) => source.Take(take);
144105

145-
return new CollectionSegment(
146-
(IReadOnlyCollection<object>)items,
147-
pageInfo,
148-
getTotalCount);
149-
}
106+
private static async ValueTask<int> CountAsync(
107+
IMongoPagingContainer<TEntity> source,
108+
CancellationToken cancellationToken) =>
109+
await source.CountAsync(cancellationToken);
150110
}
151111
}
152112
}

‎src/HotChocolate/MongoDb/test/Data.MongoDb.Paging.Tests/MongoDbCursorPagingFindFluentTests.cs

+32
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,38 @@ public async Task Simple_StringList_First_2_After()
129129
result.MatchDocumentSnapshot();
130130
}
131131

132+
[Fact]
133+
public async Task Simple_StringList_Last_1_Before()
134+
{
135+
Snapshot.FullName();
136+
137+
IRequestExecutor executor = await CreateSchemaAsync();
138+
139+
IExecutionResult result = await executor
140+
.ExecuteAsync(
141+
@"
142+
{
143+
foos(last: 1 before: ""NA=="") {
144+
edges {
145+
node {
146+
bar
147+
}
148+
cursor
149+
}
150+
nodes {
151+
bar
152+
}
153+
pageInfo {
154+
hasNextPage
155+
hasPreviousPage
156+
startCursor
157+
endCursor
158+
}
159+
}
160+
}");
161+
result.MatchDocumentSnapshot();
162+
}
163+
132164
[Fact]
133165
public async Task Simple_StringList_Global_DefaultItem_2()
134166
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"data": {
3+
"foos": {
4+
"edges": [
5+
{
6+
"node": {
7+
"bar": "e"
8+
},
9+
"cursor": "Mw=="
10+
}
11+
],
12+
"nodes": [
13+
{
14+
"bar": "e"
15+
}
16+
],
17+
"pageInfo": {
18+
"hasNextPage": true,
19+
"hasPreviousPage": true,
20+
"startCursor": "Mw==",
21+
"endCursor": "Mw=="
22+
}
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)
Please sign in to comment.