From 82ddb26c64839046c68272cf9876be9493a6c4bd Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Sun, 12 Apr 2026 19:20:36 -0400 Subject: [PATCH] Pagination --- .../Services/OrganizationClientService.cs | 10 +++--- JobFlow.Business/Utilities/CursorToken.cs | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/JobFlow.Business/Services/OrganizationClientService.cs b/JobFlow.Business/Services/OrganizationClientService.cs index 2e4536b..a8086b9 100644 --- a/JobFlow.Business/Services/OrganizationClientService.cs +++ b/JobFlow.Business/Services/OrganizationClientService.cs @@ -125,19 +125,21 @@ public async Task>> GetClients var withEmailCount = await baseQuery.CountAsync(c => c.EmailAddress != null && c.EmailAddress.Trim() != string.Empty); var withPhoneCount = await baseQuery.CountAsync(c => c.PhoneNumber != null && c.PhoneNumber.Trim() != string.Empty); - if (CursorToken.TryRead(cursor, out var cursorCreatedAt, out var cursorId)) + var offset = 0; + if (CursorToken.TryReadOffset(cursor, out var parsedOffset)) { - query = query.Where(c => c.CreatedAt < cursorCreatedAt || (c.CreatedAt == cursorCreatedAt && c.Id.CompareTo(cursorId) < 0)); + offset = parsedOffset; } var batch = await query + .Skip(offset) .Take(size + 1) .ToListAsync(); var hasMore = batch.Count > size; var items = hasMore ? batch.Take(size).ToList() : batch; - var nextCursor = hasMore && items.Count > 0 - ? CursorToken.Build(items[^1].CreatedAt, items[^1].Id) + var nextCursor = hasMore + ? CursorToken.BuildOffset(offset + size) : null; return Result.Success(new CursorPagedResponseDto diff --git a/JobFlow.Business/Utilities/CursorToken.cs b/JobFlow.Business/Utilities/CursorToken.cs index 1e997ac..7ef9aef 100644 --- a/JobFlow.Business/Utilities/CursorToken.cs +++ b/JobFlow.Business/Utilities/CursorToken.cs @@ -10,6 +10,12 @@ public static string Build(DateTime timestampUtc, Guid id) return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)); } + public static string BuildOffset(int offset) + { + var raw = $"off|{offset}"; + return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)); + } + public static bool TryRead(string? cursor, out DateTime timestampUtc, out Guid id) { timestampUtc = default; @@ -21,6 +27,11 @@ public static bool TryRead(string? cursor, out DateTime timestampUtc, out Guid i try { var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(cursor)); + + // Skip offset-style cursors + if (decoded.StartsWith("off|")) + return false; + var split = decoded.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (split.Length != 2) return false; @@ -39,4 +50,25 @@ public static bool TryRead(string? cursor, out DateTime timestampUtc, out Guid i return false; } } + + public static bool TryReadOffset(string? cursor, out int offset) + { + offset = 0; + + if (string.IsNullOrWhiteSpace(cursor)) + return false; + + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(cursor)); + if (!decoded.StartsWith("off|")) + return false; + + return int.TryParse(decoded.AsSpan(4), out offset) && offset >= 0; + } + catch + { + return false; + } + } }