Skip to content

Commit 7b86d81

Browse files
committed
Implement HasPreviousPage
1 parent 6f3988b commit 7b86d81

File tree

35 files changed

+685
-123
lines changed

35 files changed

+685
-123
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ generated_code = true
5353

5454
# C# files
5555
[*.cs]
56+
5657
# New line preferences
5758
csharp_new_line_before_open_brace = all
5859
csharp_new_line_before_else = true

lint-test-and-nuget-test.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ function RunLinterAndStandardTests{
6868
[string] $AdditionalConfiguration
6969
)
7070

71-
Exec { & dotnet format --verify-no-changes }
71+
if ($Env -eq 'ci') {
72+
Exec { & dotnet format style --verify-no-changes }
73+
Exec { & dotnet format analyzers --verify-no-changes }
74+
} else {
75+
Exec { & dotnet format --verify-no-changes }
76+
}
77+
7278
Exec { & dotnet build -c $Configuration $AdditionalConfiguration '/p:ExtraNoWarn1="JAMCP0007"' '/p:ExtraNoWarn2="JAMCP0018"' }
7379
Exec { & dotnet test -c $Configuration $AdditionalConfiguration --logger trx --no-build --no-restore }
7480
}

samples/Jameak.CursorPagination.Sample/Controllers/PaginatedDataController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public async Task<PaginatedResponse> PaginateByKeySetCursor(string? afterCursor,
3939
GetDtoQueryable(),
4040
DelegateMethods.ToListAsyncDelegate(),
4141
DelegateMethods.CountAsyncDelegate(),
42+
DelegateMethods.AnyAsyncDelegate(),
4243
afterCursor,
4344
PageSize,
4445
computeTotalCount: Enums.ComputeTotalCount.Once,
@@ -52,7 +53,8 @@ public async Task<PaginatedResponse> PaginateByKeySetCursor(string? afterCursor,
5253
// Entirely up to your use-case how much of this page metadata makes sense to compute and fill out.
5354
TotalCount = page.TotalCount!.Value,
5455
HasNextPage = page.HasNextPage!.Value,
55-
NextPageCursor = page.NextCursor == null ? null : _keySetPaginationStrategy.CursorToString(page.NextCursor)
56+
HasPreviousPage = await page.HasPreviousPageAsync(),
57+
NextPageCursor = page.NextCursor == null ? null : _keySetPaginationStrategy.CursorToString(page.NextCursor),
5658
},
5759
Data = page.Items.Select(item => new DataWithCursor
5860
{
@@ -85,6 +87,7 @@ public async Task<PaginatedResponse> PaginateByOffsetCursor(string? afterCursor,
8587
{
8688
TotalCount = page.TotalCount!.Value,
8789
HasNextPage = page.HasNextPage!.Value,
90+
HasPreviousPage = await page.HasPreviousPageAsync(),
8891
NextPageCursor = page.NextCursor?.CursorToString()
8992
},
9093
Data = page.Items.Select(item => new DataWithCursor
@@ -118,6 +121,7 @@ public async Task<PaginatedResponse> PaginateByPageNumber(int? pageNumber, Cance
118121
{
119122
TotalCount = page.TotalCount!.Value,
120123
HasNextPage = page.HasNextPage!.Value,
124+
HasPreviousPage = await page.HasPreviousPageAsync(),
121125
NextPageCursor = page.NextCursor?.CursorToString()
122126
},
123127
Data = page.Items.Select(item => new DataWithCursor

samples/Jameak.CursorPagination.Sample/DelegateMethods.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public static class DelegateMethods
77
{
88
public static ToListAsync<DtoTypeToPaginate> ToListAsyncDelegate() => (queryable, cancellationToken) => queryable.ToListAsync(cancellationToken);
99
public static CountAsync<DtoTypeToPaginate> CountAsyncDelegate() => (queryable, cancellationToken) => queryable.CountAsync(cancellationToken);
10+
public static AnyAsync<DtoTypeToPaginate> AnyAsyncDelegate() => (queryable, cancellationToken) => queryable.AnyAsync(cancellationToken);
1011
}

samples/Jameak.CursorPagination.Sample/PaginatedBatchJob.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ private async Task ProcessInBatches(CancellationToken stoppingToken)
5757
dtoQueryable,
5858
DelegateMethods.ToListAsyncDelegate(),
5959
DelegateMethods.CountAsyncDelegate(),
60+
DelegateMethods.AnyAsyncDelegate(),
6061
null,
6162
PageSize,
6263
computeTotalCount: Enums.ComputeTotalCount.Once,

samples/Jameak.CursorPagination.Sample/ResponseModels/PageInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public class PageInfo
44
{
55
public required string? NextPageCursor { get; init; }
66
public required bool HasNextPage { get; init; }
7+
public required bool HasPreviousPage { get; init; }
78
public required int TotalCount { get; init; }
89
}

src/Jameak.CursorPagination.Abstractions/Enums/KeySetCursorSerializerGeneration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ public enum KeySetCursorSerializerGeneration
2121
/// <remarks>
2222
/// The source-generated implementation uses a <b>JsonNamingPolicy</b> to hide the paginated cursors property names.
2323
/// </remarks>
24-
UseSystemTextJson = 2
24+
UseSystemTextJson = 2,
2525
}

src/Jameak.CursorPagination.Abstractions/Enums/PaginationDirection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ public enum PaginationDirection
2929
/// <para>Notice that this produces the dataset paginated backwards, but with each page representing the data in the non-reverse order.</para>
3030
/// <para>If you need the data in reverse order, instead perform a Forward pagination on the opposite sort-order instead (ascending vs. descending)</para>
3131
/// </remarks>
32-
Backward = 2
32+
Backward = 2,
3333
}

src/Jameak.CursorPagination/Delegates.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ namespace Jameak.CursorPagination;
2828
/// </remarks>
2929
public delegate Task<int> CountAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken);
3030

31+
/// <summary>
32+
/// Method used to asynchronously determine whether a <see cref="IQueryable{T}"/> contains any elements.
33+
/// </summary>
34+
/// <returns>Returns true if any elements exist.</returns>
35+
/// <remarks>
36+
/// If using EFCore for async operations you can create this async any method like so:
37+
/// <code>
38+
/// (queryable, cancellationToken) => queryable.AnyAsync(cancellationToken)
39+
/// </code>
40+
/// </remarks>
41+
public delegate Task<bool> AnyAsync<T>(IQueryable<T> queryable, CancellationToken cancellationToken);
42+
3143
internal delegate PageResult<T, TCursor> NextPage<T, TCursor>() where TCursor : ICursor;
3244

3345
internal delegate Task<PageResultAsync<T, TCursor>> NextPageAsync<T, TCursor>(CancellationToken cancellationToken) where TCursor : ICursor;

src/Jameak.CursorPagination/InternalPaginatorHelper.cs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
using System.Runtime.CompilerServices;
22
using Jameak.CursorPagination.Abstractions;
3+
using Jameak.CursorPagination.Abstractions.Enums;
34
using Jameak.CursorPagination.Enums;
45
using Jameak.CursorPagination.Page;
56

67
namespace Jameak.CursorPagination;
78
internal static class InternalPaginatorHelper
89
{
10+
internal sealed record EmptyNextPageState(
11+
int? TotalCount,
12+
Func<bool> HasPreviousPageFunc);
13+
14+
internal sealed record EmptyNextPageStateAsync(
15+
int? TotalCount,
16+
Func<Task<bool>> HasPreviousPageAsyncFunc);
17+
918
internal static NextPage<TData, TCursor> DetermineNextPageFunc<TData, TCursor, TDataEntry>(
1019
Func<TCursor, NextPage<TData, TCursor>> nextPageFuncGenerator,
1120
Func<TDataEntry, TCursor> createCursor,
1221
TDataEntry? nextCursorElement,
13-
int? totalCount,
22+
EmptyNextPageState emptyNextPageState,
1423
bool? hasNextPage,
1524
ComputeNextPage computeNextPage) where TCursor : ICursor
1625
{
1726
if (nextCursorElement == null
1827
|| CanSkipNextPageCheck(computeNextPage, hasNextPage))
1928
{
20-
return EmptyNextPage<TData, TCursor>(totalCount);
29+
return EmptyNextPage<TData, TCursor>(emptyNextPageState);
2130
}
2231

2332
return nextPageFuncGenerator(createCursor(nextCursorElement));
@@ -27,14 +36,14 @@ internal static NextPageAsync<TData, TCursor> DetermineNextPageAsyncFunc<TData,
2736
Func<TCursor, NextPageAsync<TData, TCursor>> nextPageAsyncFuncGenerator,
2837
Func<TDataEntry, TCursor> createCursor,
2938
TDataEntry? nextCursorElement,
30-
int? totalCount,
39+
EmptyNextPageStateAsync emptyNextPageState,
3140
bool? hasNextPage,
3241
ComputeNextPage computeNextPage) where TCursor : ICursor
3342
{
3443
if (nextCursorElement == null
3544
|| CanSkipNextPageCheck(computeNextPage, hasNextPage))
3645
{
37-
return EmptyNextPageAsync<TData, TCursor>(totalCount);
46+
return EmptyNextPageAsync<TData, TCursor>(emptyNextPageState);
3847
}
3948

4049
return nextPageAsyncFuncGenerator(createCursor(nextCursorElement));
@@ -49,21 +58,36 @@ private static bool CanSkipNextPageCheck(
4958
&& !hasNextPage.Value;
5059
}
5160

52-
internal static NextPage<T, TCursor> EmptyNextPage<T, TCursor>(int? totalCount) where TCursor : ICursor
61+
private static NextPage<T, TCursor> EmptyNextPage<T, TCursor>(
62+
EmptyNextPageState emptyNextPageState) where TCursor : ICursor
5363
{
54-
return () => new PageResult<T, TCursor>([], false, totalCount, EmptyNextPage<T, TCursor>(totalCount), default);
64+
return () => new PageResult<T, TCursor>(
65+
[],
66+
hasNextPage: false,
67+
emptyNextPageState.TotalCount,
68+
EmptyNextPage<T, TCursor>(emptyNextPageState),
69+
nextCursor: default,
70+
emptyNextPageState.HasPreviousPageFunc);
5571
}
5672

57-
internal static NextPageAsync<T, TCursor> EmptyNextPageAsync<T, TCursor>(int? totalCount) where TCursor : ICursor
73+
private static NextPageAsync<T, TCursor> EmptyNextPageAsync<T, TCursor>(
74+
EmptyNextPageStateAsync emptyNextPageState) where TCursor : ICursor
5875
{
59-
return (cancellationToken) => Task.FromResult(new PageResultAsync<T, TCursor>([], false, totalCount, EmptyNextPageAsync<T, TCursor>(totalCount), default));
76+
return (cancellationToken) => Task.FromResult(
77+
new PageResultAsync<T, TCursor>(
78+
[],
79+
hasNextPage: false,
80+
emptyNextPageState.TotalCount,
81+
EmptyNextPageAsync<T, TCursor>(emptyNextPageState),
82+
nextCursor: default,
83+
emptyNextPageState.HasPreviousPageAsyncFunc));
6084
}
6185

6286
internal static bool ShouldComputeTotalCount(bool hasAlreadyComputedCount, ComputeTotalCount totalEnum)
6387
{
6488
return (hasAlreadyComputedCount, totalEnum) switch
6589
{
66-
(false, ComputeTotalCount.Never) => false,
90+
(_, ComputeTotalCount.Never) => false,
6791
(false, ComputeTotalCount.Once) => true,
6892
(true, ComputeTotalCount.Once) => false,
6993
(_, ComputeTotalCount.EveryPage) => true,
@@ -78,4 +102,26 @@ internal static void ThrowIfEnumNotDefined<T>(T argument, [CallerArgumentExpress
78102
throw new ArgumentException($"Parameter '{paramName}' has invalid enum value '{argument}'.", paramName);
79103
}
80104
}
105+
106+
internal static (T? previousCursorElement, T? nextCursorElement) GetCursorElements<T>(
107+
List<T> pageData,
108+
PaginationDirection paginationDirection)
109+
{
110+
return paginationDirection switch
111+
{
112+
PaginationDirection.Forward => (pageData.FirstOrDefault(), pageData.LastOrDefault()),
113+
PaginationDirection.Backward => (pageData.LastOrDefault(), pageData.FirstOrDefault()),
114+
_ => throw new ArgumentOutOfRangeException(nameof(paginationDirection)),
115+
};
116+
}
117+
118+
internal static PaginationDirection InvertDirection(PaginationDirection paginationDirection)
119+
{
120+
return paginationDirection switch
121+
{
122+
PaginationDirection.Forward => PaginationDirection.Backward,
123+
PaginationDirection.Backward => PaginationDirection.Forward,
124+
_ => throw new ArgumentOutOfRangeException(nameof(paginationDirection)),
125+
};
126+
}
81127
}

0 commit comments

Comments
 (0)