EFPagination.AspNetCore
2.0.0
dotnet add package EFPagination.AspNetCore --version 2.0.0
NuGet\Install-Package EFPagination.AspNetCore -Version 2.0.0
<PackageReference Include="EFPagination.AspNetCore" Version="2.0.0" />
<PackageVersion Include="EFPagination.AspNetCore" Version="2.0.0" />
<PackageReference Include="EFPagination.AspNetCore" />
paket add EFPagination.AspNetCore --version 2.0.0
#r "nuget: EFPagination.AspNetCore, 2.0.0"
#:package EFPagination.AspNetCore@2.0.0
#addin nuget:?package=EFPagination.AspNetCore&version=2.0.0
#tool nuget:?package=EFPagination.AspNetCore&version=2.0.0
Entity Framework Pagination (EFPagination)
Keyset pagination for Entity Framework Core. Also known as seek or cursor pagination.
Keyset pagination delivers stable query performance regardless of page depth, unlike offset pagination (Skip/Take) which degrades linearly as you skip more rows.
Installation
dotnet add package EFPagination
For ASP.NET Core integration (optional):
dotnet add package EFPagination.AspNetCore
Requires .NET 10 and EF Core 10.
Quick Start
using EFPagination;
// Build once and store as a static readonly field.
static readonly PaginationQueryDefinition<User> Definition =
PaginationQuery.Build<User>(b => b.Descending(x => x.Created).Ascending(x => x.Id));
// First page.
var page = await dbContext.Users
.Keyset(Definition)
.TakeAsync(20);
// Next page -- pass the cursor from the previous response.
var nextPage = await dbContext.Users
.Keyset(Definition)
.After(page.NextCursor!)
.TakeAsync(20);
// Previous page.
var prevPage = await dbContext.Users
.Keyset(Definition)
.Before(nextPage.PreviousCursor!)
.TakeAsync(20);
What's Included
- Fluent keyset pagination via
.Keyset(definition).After(cursor).TakeAsync(20) CursorPage<T>with opaqueNextCursor/PreviousCursortokensIAsyncEnumerablestreaming via.Keyset(definition).StreamAsync(100)- Prebuilt definitions with cached expression tree templates for zero per-request overhead
SortField.Create<T>one-line sort field factoryPaginationSortRegistry<T>for request-driven dynamic sorting- Opaque cursor encoding/decoding with schema fingerprinting and optional HMAC signing
PaginationQuery.Build<T>(string, ...)for runtime sort definitions- Low-level
Paginate()API for customIQueryablecomposition - ASP.NET Core integration:
PaginationRequest,PaginatedResponse<T>,FromRequest,ToPaginatedResponse - Roslyn analyzers: nullable columns (KP0001), non-unique tiebreakers (KP0002), ad-hoc builders in hot paths (KP0003), missing order correction (KP0004)
API
Keyset (Fluent API)
The primary API surface. Chain .Keyset() on any IQueryable<T>:
// First page
var page = await db.Users
.Keyset(definition)
.TakeAsync(20);
// Forward from cursor
var page = await db.Users
.Keyset(definition)
.After(cursor)
.TakeAsync(20);
// Backward from cursor
var page = await db.Users
.Keyset(definition)
.Before(cursor)
.TakeAsync(20);
// With total count
var page = await db.Users
.Keyset(definition)
.After(cursor)
.IncludeCount()
.TakeAsync(20);
// With max page size clamp (defaults to 500)
var page = await db.Users
.Keyset(definition)
.After(cursor)
.MaxPageSize(100)
.TakeAsync(requestedPageSize);
// With entity reference instead of cursor
var page = await db.Users
.Keyset(definition)
.AfterEntity(lastUser)
.TakeAsync(20);
// Stream all pages
await foreach (var batch in db.Users.Keyset(definition).StreamAsync(100))
{
// process batch
}
TakeAsync returns CursorPage<T>:
| Property | Type | Description |
|---|---|---|
Items |
List<T> |
The page items in correct order. |
NextCursor |
string? |
Opaque cursor for the next page, or null when no more pages. |
PreviousCursor |
string? |
Opaque cursor for the previous page, or null on the first page. |
TotalCount |
int |
Total rows when IncludeCount() was called; otherwise -1. |
PaginationQueryDefinition<T>
A prebuilt, reusable pagination definition. Build once, reuse across requests:
static readonly PaginationQueryDefinition<User> Definition =
PaginationQuery.Build<User>(b => b.Descending(x => x.Created).Ascending(x => x.Id));
PaginationQuery
Factory for building reusable definitions:
// From a builder action (recommended):
PaginationQueryDefinition<T> Build<T>(Action<PaginationBuilder<T>> builderAction)
// From a property name string (for runtime sort fields):
PaginationQueryDefinition<T> Build<T>(
string propertyName,
bool descending,
string? tiebreaker = "Id",
bool tiebreakerDescending = false)
PaginationBuilder<T>
Fluent builder for defining pagination columns, used inside PaginationQuery.Build<T>():
| Method | Description |
|---|---|
Ascending<TCol>(Expression<Func<T, TCol>>) |
Adds a column with ascending sort. |
Descending<TCol>(Expression<Func<T, TCol>>) |
Adds a column with descending sort. |
ConfigureColumn<TCol>(Expression<Func<T, TCol>>, bool isDescending) |
Adds a column with explicit sort direction. |
Nested properties are supported: b.Ascending(x => x.Details.Created).
PaginationSortRegistry<T> / SortField
Map request sort names to prebuilt definitions:
var sorts = new PaginationSortRegistry<User>(
defaultDefinition: PaginationQuery.Build<User>(b => b.Descending(x => x.Created).Ascending(x => x.Id)),
SortField.Create<User>("created", "Created"),
SortField.Create<User>("name", "Name"));
var definition = sorts.Resolve(sortBy, sortDir);
SortField.Create<T> builds both ascending and descending definitions from a property name:
SortField<T> Create<T>(string name, string propertyName, string? tiebreaker = "Id")
Paginate (Low-Level API)
For consumers who need raw IQueryable composition (e.g., .Include(), .Select(), custom materialization):
var context = db.Users.Paginate(definition, direction, reference);
var users = await context.Query
.Include(x => x.Details)
.Take(20)
.ToListAsync();
PaginationContext<T> exposes:
| Property | Description |
|---|---|
Query |
The filtered and ordered IQueryable<T>. |
OrderedQuery |
The ordered IQueryable<T> without the pagination filter. |
Direction |
The PaginationDirection used for this call. |
PaginationCursor
Encode and decode opaque cursor tokens:
string Encode(ReadOnlySpan<ColumnValue> values, PaginationCursorOptions? options = null);
bool TryDecode<T>(ReadOnlySpan<char> encoded, PaginationQueryDefinition<T> definition, out PaginationValues<T> values, out int written)
PaginationCursorOptions supports SchemaFingerprint (stale cursor rejection) and SigningKey (HMAC verification).
PaginationDirection
enum PaginationDirection { Forward, Backward }
IncompatibleReferenceException
Thrown when a reference object is missing a property required by the pagination definition.
EFPagination.AspNetCore
PaginationRequest
Binds cursor-based pagination parameters from the query string:
public readonly record struct PaginationRequest(
string? After = null,
string? Before = null,
int PageSize = 25,
string? SortBy = null,
string? SortDir = null);
Fluent Integration
Use FromRequest to apply cursors from a request, or Keyset(registry, request) for dynamic sorting:
// Fixed definition
var page = await db.Users
.Keyset(definition)
.FromRequest(request)
.MaxPageSize(100)
.TakeAsync(request.PageSize);
return page.ToPaginatedResponse(u => new UserDto(u.Id, u.Name));
// Dynamic sorting via registry
var page = await db.Users
.Keyset(sortRegistry, request)
.MaxPageSize(100)
.TakeAsync(request.PageSize);
return page.ToPaginatedResponse(u => new UserDto(u.Id, u.Name));
PaginatedResponse<T>
JSON-serializable response envelope:
public sealed record PaginatedResponse<T>(
IReadOnlyList<T> Items,
string? NextCursor,
string? PreviousCursor,
int? TotalCount);
ToPaginatedResponse
Convert a CursorPage<T> to a PaginatedResponse<T>:
PaginatedResponse<T> ToPaginatedResponse<T>(this CursorPage<T> page);
PaginatedResponse<TOut> ToPaginatedResponse<T, TOut>(this CursorPage<T> page, Func<T, TOut> selector)
Documentation
| Topic | Description |
|---|---|
| Getting Started | Installation, requirements, first query |
| Pagination Patterns | First, last, previous, next page patterns |
| API Reference | Full API details |
| Prebuilt Definitions | Caching pagination definitions for performance |
| Database Indexing | Composite indexes, deterministic definitions |
| NULL Handling | Computed columns, expression coalescing |
| Loose Typing | DTOs, projections, anonymous type references |
| Analyzers & Diagnostics | Build-time warnings and fixes |
Samples
The samples directory contains a Razor Pages demo with four pagination variations and Minimal API endpoints showcasing cursor-based pagination with sort registries.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- EFPagination (>= 2.0.0)
- Microsoft.EntityFrameworkCore (>= 10.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0 | 94 | 3/28/2026 |
See https://github.com/DrSkillIssue/EFPagination/releases for release notes.