An allow-list, not a query language
A field that is not declared is not addressable, and an operator not granted for a field is rejected for that field. No accidental ORDER BY on an unindexed column.
Configure a resource
Declare once per entity what clients may sort, search and filter. The engine turns an opinionated query string into a translated EF Core query, and rejects everything it cannot honour.
A field that is not declared is not addressable, and an operator not granted for a field is rejected for that field. No accidental ORDER BY on an unindexed column.
Configure a resource
$eq $in $null $sw $ilike $contains $lt $lte $gt $gte $btw, with $not negation and $and / $or between criteria on the same field.
See the emitted SQL
A tie-breaker key is appended to every sort, so rows cannot drift between pages. It is required — a configuration without one does not build.
How sorting works
From a DTO built for you by reflection to a hand-written selector with sub-collections and aggregates, each keeping the SELECT narrow.
Pick a strategy
Query-string binding, ProblemDetails on bad input, first / prev / next / last links, and OpenAPI parameters generated from the same config the engine enforces.
Wire up an endpoint
Ceilings on page size, filter values, filter conditions, sort fields and search length, so one request cannot cost the database an afternoon.
See the defaults
GET /products?page=2&limit=25&sortBy=price:DESC&search=widget&filter.status=$in:Active,Draft&filter.price=$btw:10,500That contract is borrowed from nestjs-paginate (MIT) — the same query parameters, operator names and response envelope.
Versions track the framework: the first component is the .NET / EF Core major the package targets, so this is the 10.x line, pairing with .NET 10 and EF Core 10. Older lines are not maintained in parallel, and release notes live on the Releases page.
This site is versioned — check which copy you are reading
Nothing here describes unreleased code: the site is published when a stable release is, so the root always shows the newest one — and a prerelease you installed with --prerelease may be ahead of what it says. Every released line also keeps its own copy at /v<line>.x/, this one included, and the version picker in the navigation moves between them without losing your place on the page. From 10.1.0 on, the links inside a package README name that package's own line, so a reader arriving from nuget.org lands in the right copy already. The 10.0.x READMEs predate the versioned copies and point here, to the root — if you installed one of those, open /v10.0.x/ from the picker to read what shipped with it.
Within a line the third component only ever adds, so a member can be newer than the package you installed but never gone from it. One that arrived after the line opened carries its version beside its heading —
10.1.0 — so the gap is visible without leaving the page; an unmarked memberpredates the line. The Releases page has what each one carried.
Four packages. The engine works on its own against any IQueryable<T>; the three add-ons build on it and are independent of each other, so take only the ones you need.
| Package | Purpose |
|---|---|
Janzen.Pagination.EntityFrameworkCore | Provider-agnostic query engine — fluent PaginateConfig<T>, filtering / sorting / search, projection, PaginateAsync. |
Janzen.Pagination.PostgreSql | PostgreSQL provider — case-insensitive search via native ILIKE. |
Janzen.Pagination.AspNetCore | ASP.NET Core integration — query-string model binding, ProblemDetails, links, OpenAPI metadata. |
Janzen.Pagination.NodaTime | NodaTime support — filter / sort / project the seven NodaTime value types (incl. Instant → DateTimeOffset). |
dotnet add package Janzen.Pagination.EntityFrameworkCore
dotnet add package Janzen.Pagination.AspNetCoreThe guide is meant to be read in order, but each page stands on its own. Getting started takes it from install to a paginated endpoint returning JSON.
Once it runs, the site splits by what you are doing:
| Query-string contract | Every parameter and operator, with the SQL each one produces. The response, the config API, the query composers and the 400 catalogue sit beside it. |
| Cookbook | Task-shaped: role-based configs, collection filters, aggregates, non-web callers, indexing, testing, troubleshooting. |
| Integrations | What changes when you add ASP.NET Core, PostgreSQL, NodaTime, or a type of your own. |