OrionPage
Pagination that stays fast on page 10,000. Keyset (cursor) paging for EF Core with an opaque cursor and a one-line query extension — because OFFSET 100000 is a table scan, and every list endpoint eventually pays for it.
Almost every list endpoint ships .Skip(page * size).Take(size). It works in the demo and rots in production: OFFSET 100000 LIMIT 20 makes the database read and discard 100,000 rows every time (deep pages get linearly slower), and inserting a row mid-scan shifts everything so clients skip or duplicate records. The correct fix — keyset paging (WHERE (created, id) < (@c, @i) ORDER BY created DESC, id DESC LIMIT 20) — is constant-time and stable, but composing the tuple predicate, encoding a multi-column cursor, and handling ties is fiddly enough that most teams don't, or do it wrong. OrionPage owns that mechanism so nobody hand-writes tuple-comparison predicates again.
Packages
OrionPage— the framework-free core:Page<T>and the opaqueCursorcodec. Reflection-free and AOT-clean.OrionPage.EntityFrameworkCore— theToKeysetPageAsyncquery extension plus the keyset engine (KeysetPredicateBuilder,KeysetSortKey): reads yourOrderBychain, builds the tuple-comparison predicate, and executes. (Not AOT-published — the predicate builder relies on operator-method reflection that NativeAOT trims, and EF Core is not AOT-clean.)
Install
dotnet add package OrionPage.EntityFrameworkCore
Quick start
Order the query (with OrderBy/ThenBy as the final operators, ending in a unique tie-breaker like Id), then page it:
using Moongazing.OrionPage;
using Moongazing.OrionPage.EntityFrameworkCore;
public Task<Page<Order>> ListAsync(string? cursor, CancellationToken ct) =>
db.Orders
.OrderByDescending(o => o.CreatedUtc).ThenByDescending(o => o.Id) // stable total ordering
.ToKeysetPageAsync(cursor, pageSize: 20, ct);
Page<T> carries the rows, an opaque forward NextCursor, and HasMore:
{ "items": [ /* ... */ ], "nextCursor": "eyJjIjoiMjAyNi0wNy0...", "hasMore": true }
Fetch the next page by feeding NextCursor back in. When HasMore is false, NextCursor is null.
Projecting to a DTO
Order and page the entity, then map the returned items — the always-translatable shape:
var page = await db.Orders
.OrderByDescending(o => o.CreatedUtc).ThenByDescending(o => o.Id)
.ToKeysetPageAsync(cursor, 20, ct);
var dtos = page.Items.Select(o => new OrderDto(o.Id, o.CreatedUtc, o.Total)).ToList();
You can also project before ordering when EF can translate the ordering — an anonymous-type projection ordered by its members works; ordering through a positional-record constructor does not.
What it guarantees
- Constant-time deep pages. A continuation is a
WHEREtuple-comparison seek against your sort index — neverOFFSET. Page 10,000 costs the same as page 1. - No skipped or duplicated rows across concurrent writes, provided the ordering is a stable total order — which is why the last sort key must be a unique tie-breaker (e.g.
Id). Ties on earlier keys are handled correctly. - Opaque cursors. The cursor is a compact, URL-safe, reflection-free binary token; a malformed or tampered cursor is a typed
InvalidCursorException(mapped to400by the Wave 3 web binding), not a 500. HMAC signing lands in a later wave. - No unbounded
COUNT(*).HasMoreanswers "is there a next page?" without the O(n) cost of a total count.
Observability
A Moongazing.OrionPage activity source emits an OrionPage.keyset span per page with orion.page.size and orion.page.has_more attributes, on the family's OrionInstrumentation spine.
Roadmap
This is the Wave 1 keyset engine (v0.1): keyset paging over EF Core, an opaque cursor, and multi-column ordering read from the OrderBy chain, AOT-clean. Later waves add HMAC-signed cursors and nullable/multi-directional sort-key hardening (W2), the minimal-API [AsCursor] binding + .WithKeysetPaging() filter + OpenAPI + OrionEnvelope meta.page (W3, GA), and bidirectional cursors + an offset-compat shim + a Dapper adapter (W4). See CHANGELOG.md.
Supported sort-key types in this release are the integer, floating-point, decimal, and date/time types; string, Guid, and nullable keys are hardened in Wave 2. OrionPage does not do "jump to page N" (keyset is sequential by nature — that is the anti-pattern being retired), does not compute a total count by default, and paginates whatever IQueryable it is given (compose authorization filters upstream).
Versioning
Follows Semantic Versioning. Multi-targets net8.0, net9.0, and net10.0. Binds to Orion.Abstractions 1.x; the EF Core extension requires EF Core 8+. The framework-free core is AOT- and trim-clean (verified by a native-binary smoke test in CI); the EF Core extension is not NativeAOT-published because EF Core itself is not AOT-clean.
Documentation
- CHANGELOG.md — release notes.
Contributing
Contributions are welcome. See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.
More from the Orion family
Focused .NET libraries built to one quality bar. Each is usable on its own; several share the small Orion.Abstractions contracts spine, but there is no deep dependency web — pick only what you need:
- Orion.Abstractions — the shared contracts spine: telemetry, options, result, clock
- OrionClock — a
TimeProvider-based clock with TTL / deadline vocabulary - OrionResult — Result/Option types and a shared error vocabulary
- OrionResilience — retry, backoff, and timeout on OrionClock
- OrionRate — rate limiting (token-bucket / sliding-window) on OrionClock
- OrionGuard — validation, guard clauses, DDD primitives, domain events
- OrionAudit — automatic EF Core change-audit trail
- OrionBeacon — leader election with fencing tokens
- OrionGrant — permission / authorization checks
- OrionInbox — transactional inbox for exactly-once effects
- OrionKey — source-generated strongly-typed IDs
- OrionLedger — API-key issuance, verification, and rotation
- OrionLens — ambient correlation-context propagation
- OrionLock — distributed locks with fencing tokens
- OrionOnce — idempotency keys for exactly-once request handling
- OrionPatch — transactional outbox for EF Core
- OrionRelay — outbound webhook delivery (HMAC, retries, backoff)
- OrionSaga — sagas / process managers for long-running workflows
- OrionShade — sensitive-data redaction for logs and telemetry
- OrionStream — server-sent events / streaming hub
- OrionVault — field-level encryption for EF Core
See it all working together in OrionShowcase, a production-shaped banking sample.
License
MIT.
Packages
| Package | Version | Downloads |
|---|---|---|
| OrionPage | 0.1.0 | 136 |
| OrionPage.EntityFrameworkCore | 0.1.0 | 115 |