OrionPage 0.1.0

dotnet add package OrionPage --version 0.1.0
                    
NuGet\Install-Package OrionPage -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OrionPage" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OrionPage" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="OrionPage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OrionPage --version 0.1.0
                    
#r "nuget: OrionPage, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OrionPage@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OrionPage&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=OrionPage&version=0.1.0
                    
Install as a Cake Tool

<p align="center"> <img src="docs/logo.png" alt="OrionPage" width="150" /> </p>

OrionPage

CI/CD NuGet

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 opaque Cursor codec. Reflection-free and AOT-clean.
  • OrionPage.EntityFrameworkCore — the ToKeysetPageAsync query extension plus the keyset engine (KeysetPredicateBuilder, KeysetSortKey): reads your OrderBy chain, 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 WHERE tuple-comparison seek against your sort index — never OFFSET. 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 to 400 by the Wave 3 web binding), not a 500. HMAC signing lands in a later wave.
  • No unbounded COUNT(*). HasMore answers "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

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.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on OrionPage:

Package Downloads
OrionPage.EntityFrameworkCore

EF Core keyset pagination for OrionPage. ToKeysetPageAsync reads the query's OrderBy/ThenBy chain, builds the tuple-comparison WHERE predicate, and returns a Page<T> with an opaque forward cursor — constant-time deep paging where Skip/Take degrades linearly, with no OFFSET.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 137 8/9/2026