feat: add lazy pagination (AsyncPageable, Page, and strategy helpers)#9
Merged
Conversation
Adds core pagination support to Dexpace.Sdk.Core: - `Page<T>` — immutable carrier of Values, Status, and Headers for one HTTP page. - `AsyncPageable<T>` — abstract base implementing IAsyncEnumerable<T> with an AsPages() view for per-page metadata. - `Pageable.Create<TPage,T>` — factory that builds a lazy pipeline-driven pageable; each page is fetched on-demand through the full HttpPipeline (retry, auth, telemetry apply per page). nextRequest drives iteration; maxPages caps it. Responses are disposed inside a try/finally before yielding each Page<T> so headers are safely captured first. 19 new tests in Dexpace.Sdk.Core.Tests/Pagination/ covering: item flattening, AsPages count/values/metadata, laziness (single send after first-page consume, second send on advance), maxPages cap, null-nextRequest termination, response body disposal, Page<T> constructor guards, and Pageable.Create null-arg guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add four test scenarios to PageableTests that were missing coverage: early-break disposal (first body disposed, transport called once), exception-path disposal (serde throws but body is still disposed), re-enumeration (each iteration restarts from first, independent call count), and already-cancelled token (items + pages paths both surface OperationCanceledException before the transport is driven). The last scenario required a small correctness fix to PagesCore: add ThrowIfCancellationRequested() at the top of each iteration so a pre-cancelled token is observed before SendAsync is called. Also adds a one-line XML remarks note on the AsPages override stating that pageSizeHint is not plumbed in v1 and that cancellation for the pages path is passed via WithCancellation(token). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add public static class PaginationStrategies with three factory methods:
- Cursor<TPage>: reads a cursor string from the deserialized page and
sets it as a URL query parameter on the next request; returns null
when the cursor is absent or empty to end iteration.
- PageNumber<TPage>: reads the current integer value of a query
parameter (defaults to 1 if absent), increments it, and returns the
next request while hasMore(page) is true.
- LinkHeader<TPage>: parses the Link response header per RFC 8288
(comma-separated <url>; rel="…" entries, handles top-level commas
inside angle brackets), resolves the matching URL against the current
request URL via Uri.TryCreate, and stops on parse failure or missing
rel. Default rel is "next".
All helpers are BCL-only (no new package references). Internal query
manipulation uses a zero-allocation ref-struct QueryPairEnumerator and
UriBuilder. Each strategy produces current with { Url = newUrl },
preserving method, headers, and body.
Covered by PaginationStrategiesTests: cursor set/stop/replace, cursor
preserves method+headers, page-number increment/stop/absent-default,
link-header next/stop/relative/missing/multi-entry/malformed/default-rel,
and one end-to-end Pageable.Create + cursor strategy two-page walk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eme guard)
Three correctness issues in PaginationStrategies.LinkHeader:
1. Quote-aware splitting — the top-level comma split (SplitLinkEntries) and the
per-entry semicolon split (new SplitLinkParams) now suppress splitting while
inside a double-quoted run, so a comma or semicolon inside a param value like
title=\"Page 3, final\" no longer breaks the entry and stops pagination early.
2. Multi-valued rel — RFC 8288 specifies rel as a space-separated token list.
The rel value is now split on whitespace and any matching token (case-insensitive)
is accepted, so rel=\"prev next\" correctly matches a \"next\" lookup.
3. Scheme guard — after resolving the link URL against the current request URL,
returns null unless the resolved URI is absolute with an http or https scheme.
This prevents mailto:, ftp:, javascript:, etc. from reaching the request layer,
where current with { Url = ... } would bypass Request's constructor validation.
Nine new tests added covering all three fixes plus fragment/sibling-param
preservation and body carryover for both Cursor and PageNumber strategies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds lazy, pipeline-driven pagination — item- and page-level async iteration over paginated operations.
AsyncPageable<T> : IAsyncEnumerable<T>withAsPages(pageSizeHint?)— iterate items flat or page-by-page. Fully lazy (exactly one request per page, fetched only as the consumer advances), disposes each response on every path (normal, exception, early break), threads cancellation, and is safely re-enumerable.Page<T>—Values+Status+Headers.Pageable.Create<TPage, T>(pipeline, first, serde, options, selectItems, nextRequest, maxPages?)— typed selectors (strongly typed, AOT-friendly; no dynamic JSON), runs each page request through theHttpPipeline, with amaxPagessafety cap.PaginationStrategies— ready-madenextRequestbuilders:Cursor(body cursor → query param),PageNumber(increment a query param while more pages remain), andLinkHeader(RFC 8288Linkwithrel="next", quote-aware parsing, multi-valuedrel, and an http/https scheme guard on the resolved target).Corestays BCL-only here; no new dependencies. Deferred to a follow-up (v1 omissions, per the slice spec):Page.ContinuationToken/ token-based resumption andpageSizeHintplumbing.Test plan
dotnet build -c Releaseclean on net8.0 + net10.0 (warnings-as-errors)dotnet format --verify-no-changescleandotnet test -c Releasepasses (301 tests: 284 core, 17 serialization)🤖 Generated with Claude Code