Skip to content

Commit d0c528c

Browse files
committed
Added page based connection type (#8132)
1 parent f4078e1 commit d0c528c

File tree

39 files changed

+858
-1156
lines changed

39 files changed

+858
-1156
lines changed

src/All.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<Project Path="HotChocolate/Core/src/Subscriptions/HotChocolate.Subscriptions.csproj" />
9090
<Project Path="HotChocolate/Core/src/Types.Analyzers/HotChocolate.Types.Analyzers.csproj" />
9191
<Project Path="HotChocolate/Core/src/Types.CursorPagination/HotChocolate.Types.CursorPagination.csproj" />
92+
<Project Path="HotChocolate/Core/src/Types.CursorPagination.Extensions/HotChocolate.Types.CursorPagination.Extensions.csproj" />
9293
<Project Path="HotChocolate/Core/src/Types.Errors/HotChocolate.Types.Errors.csproj" />
9394
<Project Path="HotChocolate/Core/src/Types.Json/HotChocolate.Types.Json.csproj" />
9495
<Project Path="HotChocolate/Core/src/Types.Mutations/HotChocolate.Types.Mutations.csproj" />

src/GreenDonut/src/GreenDonut.Data.EntityFramework/Extensions/PagingQueryableExtensions.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,7 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
155155

156156
if (arguments.EnableRelativeCursors && cursor?.IsRelative == true)
157157
{
158-
if ((arguments.Last is not null && cursor.Offset > 0) ||
159-
(arguments.First is not null && cursor.Offset < 0))
158+
if ((arguments.Last is not null && cursor.Offset > 0) || (arguments.First is not null && cursor.Offset < 0))
160159
{
161160
throw new ArgumentException(
162161
"Positive offsets are not allowed with `last`, and negative offsets are not allowed with `first`.",
@@ -237,7 +236,7 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
237236
}
238237

239238
var pageIndex = CreateIndex(arguments, cursor, totalCount);
240-
return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, pageIndex, totalCount);
239+
return CreatePage(builder.ToImmutable(), arguments, keys, fetchCount, pageIndex, requestedCount, totalCount);
241240
}
242241

243242
/// <summary>
@@ -433,12 +432,6 @@ public static async ValueTask<Dictionary<TKey, Page<TValue>>> ToBatchPageAsync<T
433432
includeTotalCount = true;
434433
}
435434

436-
Dictionary<TKey, int>? counts = null;
437-
if (includeTotalCount)
438-
{
439-
counts = await GetBatchCountsAsync(source, keySelector, cancellationToken);
440-
}
441-
442435
source = QueryHelpers.EnsureOrderPropsAreSelected(source);
443436
source = QueryHelpers.EnsureGroupPropsAreSelected(source, keySelector);
444437

@@ -447,6 +440,12 @@ public static async ValueTask<Dictionary<TKey, Page<TValue>>> ToBatchPageAsync<T
447440
// and to create a new expression that will not contain it anymore.
448441
var ordering = ExtractAndRemoveOrder(source.Expression);
449442

443+
Dictionary<TKey, int>? counts = null;
444+
if (includeTotalCount)
445+
{
446+
counts = await GetBatchCountsAsync(source, keySelector, cancellationToken);
447+
}
448+
450449
var forward = arguments.Last is null;
451450
var requestedCount = int.MaxValue;
452451
var batchExpression =
@@ -503,6 +502,7 @@ public static async ValueTask<Dictionary<TKey, Page<TValue>>> ToBatchPageAsync<T
503502
keys,
504503
item.Items.Count,
505504
pageIndex,
505+
requestedCount,
506506
totalCount);
507507
map.Add(item.Key, page);
508508
}
@@ -525,8 +525,7 @@ private static async Task<Dictionary<TKey, int>> GetBatchCountsAsync<TElement, T
525525
return await query.ToDictionaryAsync(t => t.Key, t => t.Count, cancellationToken);
526526
}
527527

528-
private static Expression<Func<IGrouping<TKey, TElement>, CountResult<TKey>>> GetOrCreateCountSelector<TElement,
529-
TKey>()
528+
private static Expression<Func<IGrouping<TKey, TElement>, CountResult<TKey>>> GetOrCreateCountSelector<TElement, TKey>()
530529
{
531530
return (Expression<Func<IGrouping<TKey, TElement>, CountResult<TKey>>>)
532531
_countExpressionCache.GetOrAdd(
@@ -572,6 +571,7 @@ private static Page<T> CreatePage<T>(
572571
CursorKey[] keys,
573572
int fetchCount,
574573
int? index,
574+
int? requestedPageSize,
575575
int? totalCount)
576576
{
577577
var hasPrevious = false;
@@ -605,14 +605,15 @@ private static Page<T> CreatePage<T>(
605605
hasNext = true;
606606
}
607607

608-
if (arguments.EnableRelativeCursors && totalCount is not null)
608+
if (arguments.EnableRelativeCursors && totalCount is not null && requestedPageSize is not null)
609609
{
610610
return new Page<T>(
611611
items,
612612
hasNext,
613613
hasPrevious,
614614
(item, o, p, c) => CursorFormatter.Format(item, keys, new CursorPageInfo(o, p, c)),
615615
index ?? 1,
616+
requestedPageSize.Value,
616617
totalCount.Value);
617618
}
618619

src/GreenDonut/src/GreenDonut.Data.Primitives/GreenDonut.Data.Primitives.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<InternalsVisibleTo Include="GreenDonut.Data" />
1112
<InternalsVisibleTo Include="GreenDonut.Data.EntityFramework" />
1213
</ItemGroup>
1314

src/GreenDonut/src/GreenDonut.Data.Primitives/Page.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ public sealed class Page<T> : IEnumerable<T>
1515
private readonly bool _hasNextPage;
1616
private readonly bool _hasPreviousPage;
1717
private readonly Func<T, int, int, int, string> _createCursor;
18-
private readonly int? _totalCount;
18+
private readonly int? _requestedPageSize;
1919
private readonly int? _index;
20+
private readonly int? _totalCount;
2021

2122
/// <summary>
2223
/// Initializes a new instance of the <see cref="Page{T}"/> class.
@@ -46,7 +47,7 @@ public Page(
4647
_items = items;
4748
_hasNextPage = hasNextPage;
4849
_hasPreviousPage = hasPreviousPage;
49-
_createCursor = (item, _, _, _) => createCursor(item);
50+
_createCursor = (item, _, _, _) => createCursor(item);
5051
_totalCount = totalCount;
5152
}
5253

@@ -71,20 +72,25 @@ public Page(
7172
/// <param name="index">
7273
/// The index number of this page.
7374
///</param>
75+
/// <param name="requestedPageSize">
76+
/// The requested page size.
77+
/// </param>
7478
internal Page(
7579
ImmutableArray<T> items,
7680
bool hasNextPage,
7781
bool hasPreviousPage,
7882
Func<T, int, int, int, string> createCursor,
7983
int index,
84+
int requestedPageSize,
8085
int totalCount)
8186
{
8287
_items = items;
8388
_hasNextPage = hasNextPage;
8489
_hasPreviousPage = hasPreviousPage;
8590
_createCursor = createCursor;
86-
_totalCount = totalCount;
8791
_index = index;
92+
_requestedPageSize = requestedPageSize;
93+
_totalCount = totalCount;
8894
}
8995

9096
/// <summary>
@@ -113,15 +119,21 @@ internal Page(
113119
public bool HasPreviousPage => _hasPreviousPage;
114120

115121
/// <summary>
116-
/// Gets the total count of items in the dataset.
117-
/// This value can be null if the total count is unknown.
122+
/// Gets the index number of this page.
118123
/// </summary>
119-
public int? TotalCount => _totalCount;
124+
public int? Index => _index;
120125

121126
/// <summary>
122-
/// Gets the index number of this page.
127+
/// Gets the requested page size.
128+
/// This value can be null if the page size is unknown.
123129
/// </summary>
124-
public int? Index => _index;
130+
internal int? RequestedSize => _requestedPageSize;
131+
132+
/// <summary>
133+
/// Gets the total count of items in the dataset.
134+
/// This value can be null if the total count is unknown.
135+
/// </summary>
136+
public int? TotalCount => _totalCount;
125137

126138
/// <summary>
127139
/// Creates a cursor for an item of this page.
@@ -136,7 +148,7 @@ internal Page(
136148

137149
public string CreateCursor(T item, int offset)
138150
{
139-
if(_index is null || _totalCount is null)
151+
if (_index is null || _totalCount is null)
140152
{
141153
throw new InvalidOperationException("This page does not allow relative cursors.");
142154
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System.Collections.Immutable;
2+
3+
namespace GreenDonut.Data;
4+
5+
/// <summary>
6+
/// Extensions for the <see cref="Page{T}"/> class.
7+
/// </summary>
8+
public static class GreenDonutPageExtensions
9+
{
10+
/// <summary>
11+
/// Creates a relative cursor for backwards pagination.
12+
/// </summary>
13+
/// <param name="page">
14+
/// The page to create cursors for.
15+
/// </param>
16+
/// <param name="maxCursors">
17+
/// The maximum number of cursors to create.
18+
/// </param>
19+
/// <returns>
20+
/// An array of cursors.
21+
/// </returns>
22+
/// <exception cref="ArgumentNullException">
23+
/// Thrown if the page is null.
24+
/// </exception>
25+
/// <exception cref="ArgumentOutOfRangeException">
26+
/// Thrown if the maximum number of cursors is less than 0.
27+
/// </exception>
28+
/// <exception cref="InvalidOperationException">
29+
/// Thrown if the page does not allow relative cursors.
30+
/// </exception>
31+
/// <remarks>
32+
/// This method creates cursors for the previous pages based on the current page.
33+
/// The cursors are created using the <see cref="Page{T}.CreateCursor(T, int)"/> method.
34+
/// </remarks>
35+
public static ImmutableArray<PageCursor> CreateRelativeBackwardCursors<T>(this Page<T> page, int maxCursors = 5)
36+
{
37+
if (page == null)
38+
{
39+
throw new ArgumentNullException(nameof(page));
40+
}
41+
42+
if (maxCursors < 0)
43+
{
44+
throw new ArgumentOutOfRangeException(
45+
nameof(maxCursors),
46+
"Max cursors must be greater than or equal to 0.");
47+
}
48+
49+
if (page.First is null || page.Index is null || page.Index == 1)
50+
{
51+
return [];
52+
}
53+
54+
var previousPages = page.Index.Value - 1;
55+
var cursors = ImmutableArray.CreateBuilder<PageCursor>();
56+
57+
maxCursors *= -1;
58+
59+
for (var i = 0; i > maxCursors && previousPages + i - 1 >= 0; i--)
60+
{
61+
cursors.Insert(
62+
0,
63+
new PageCursor(
64+
page.CreateCursor(page.First, i),
65+
previousPages + i));
66+
}
67+
68+
return cursors.ToImmutable();
69+
}
70+
71+
/// <summary>
72+
/// Creates a relative cursor for forwards pagination.
73+
/// </summary>
74+
/// <param name="page">
75+
/// The page to create cursors for.
76+
/// </param>
77+
/// <param name="maxCursors">
78+
/// The maximum number of cursors to create.
79+
/// </param>
80+
/// <returns>
81+
/// An array of cursors.
82+
/// </returns>
83+
/// <exception cref="ArgumentNullException">
84+
/// Thrown if the page is null.
85+
/// </exception>
86+
/// <exception cref="ArgumentOutOfRangeException">
87+
/// Thrown if the maximum number of cursors is less than 0.
88+
/// </exception>
89+
/// <exception cref="InvalidOperationException">
90+
/// Thrown if the page does not allow relative cursors.
91+
/// </exception>
92+
/// <remarks>
93+
/// This method creates cursors for the next pages based on the current page.
94+
/// The cursors are created using the <see cref="Page{T}.CreateCursor(T, int)"/> method.
95+
/// </remarks>
96+
public static ImmutableArray<PageCursor> CreateRelativeForwardCursors<T>(this Page<T> page, int maxCursors = 5)
97+
{
98+
if (page == null)
99+
{
100+
throw new ArgumentNullException(nameof(page));
101+
}
102+
103+
if (maxCursors < 0)
104+
{
105+
throw new ArgumentOutOfRangeException(
106+
nameof(maxCursors),
107+
"Max cursors must be greater than or equal to 0.");
108+
}
109+
110+
if (page.Last is null || page.Index is null)
111+
{
112+
return [];
113+
}
114+
115+
var totalPages = (page.TotalCount ?? 0) / (page.RequestedSize ?? 10);
116+
117+
if (page.Index >= totalPages)
118+
{
119+
return [];
120+
}
121+
122+
var cursors = ImmutableArray.CreateBuilder<PageCursor>();
123+
cursors.Add(new PageCursor(page.CreateCursor(page.Last, 0), page.Index.Value + 1));
124+
125+
for (int i = 1; i < maxCursors && page.Index + i < totalPages; i++)
126+
{
127+
cursors.Add(
128+
new PageCursor(
129+
page.CreateCursor(page.Last, i),
130+
page.Index.Value + i + 1));
131+
}
132+
133+
return cursors.ToImmutable();
134+
}
135+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace GreenDonut.Data;
2+
3+
public record PageCursor(string Cursor, int Page);

src/HotChocolate/Caching/test/Caching.Tests/HotChocolate.Caching.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17+
<ProjectReference Include="..\..\..\Core\src\Types.OffsetPagination\HotChocolate.Types.OffsetPagination.csproj" />
1718
<ProjectReference Include="..\..\src\Caching\HotChocolate.Caching.csproj" />
1819
<ProjectReference Include="..\..\..\AspNetCore\src\AspNetCore\HotChocolate.AspNetCore.csproj" />
1920
<ProjectReference Include="..\..\..\Core\test\Utilities\HotChocolate.Tests.Utilities.csproj" />

0 commit comments

Comments
 (0)