Janzen.Pagination.AspNetCore
10.1.0
Prefix Reserved
dotnet add package Janzen.Pagination.AspNetCore --version 10.1.0
NuGet\Install-Package Janzen.Pagination.AspNetCore -Version 10.1.0
<PackageReference Include="Janzen.Pagination.AspNetCore" Version="10.1.0" />
<PackageVersion Include="Janzen.Pagination.AspNetCore" Version="10.1.0" />
<PackageReference Include="Janzen.Pagination.AspNetCore" />
paket add Janzen.Pagination.AspNetCore --version 10.1.0
#r "nuget: Janzen.Pagination.AspNetCore, 10.1.0"
#:package Janzen.Pagination.AspNetCore@10.1.0
#addin nuget:?package=Janzen.Pagination.AspNetCore&version=10.1.0
#tool nuget:?package=Janzen.Pagination.AspNetCore&version=10.1.0
Janzen.Pagination.AspNetCore
ASP.NET Core integration for Janzen.Pagination.
Wires the pagination engine into the web pipeline:
- Query-string model binding — bind
PaginateQuerystraight from the request (?page=&limit=&sortBy=&search=&filter.<field>=$op:value). ProblemDetailserror handling — invalid queries surface as consistent400responses.- Pagination links —
first/previous/next/lastbuilt from the current request. - OpenAPI metadata — documents the pagination query parameters on annotated endpoints; per-field badges
(configured via
.ShowBadge(...)) render as chips in the API reference UI, colorable via your custom CSS. Fields gated with.When(...)stay documented (widest surface) and are enforced at runtime.
The contract it binds is borrowed from nestjs-paginate (MIT) — the same parameters, operator names and response envelope.
Install
dotnet add package Janzen.Pagination.AspNetCore
Requires Janzen.Pagination.EntityFrameworkCore
(referenced transitively).
Usage
// Program.cs — register the query-string model binder and the 400 ProblemDetails filter.
services.AddPagination(pagination => pagination.AddAspNetCore());
// Register the OpenAPI operation transformer on your document so the pagination query
// parameters are documented on endpoints annotated with [PaginatedQuery<TConfigProvider>].
// (The transformer is a normal IOpenApiOperationTransformer — your app owns the document name.)
using Janzen.Pagination.AspNetCore.OpenApi;
services.AddOpenApi(options => options.AddOperationTransformer<PaginatedQueryOperationTransformer>());
// Controller — PaginateQuery is bound from the query string. MVC binds the CancellationToken to
// HttpContext.RequestAborted for free; pass it so a disconnected client stops the database work.
[HttpGet]
[PaginatedQuery<ProductConfigProvider>]
public Task<PaginatedResponse<ProductDto>> Get([FromQuery] PaginateQuery request, CancellationToken ct) =>
_dbContext.Products.PaginateAsync<Product, ProductDto>(request, _config, HttpContext.Request, ct);
Minimal API
app.MapGet("/products", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
await db.Products.PaginateAsync<Product, ProductDto>(http.Request.ToPaginateQuery(), config, http.Request, ct))
.WithPagination<ProductConfigProvider>();
WithPagination<TConfigProvider>() attaches the OpenAPI pagination parameters and the documented 400
response, and maps PaginateQueryException to a 400 Problem Details via an endpoint filter.
Request.ToPaginateQuery() builds the PaginateQuery from the query string.
Links and the Link header
Passing the HttpRequest to a Paginate*Async call is what fills response.Links with
first/previous/next/last and current, built from the current request: path-relative — including the
app's UsePathBase prefix — with every other query parameter preserved. An absent link (previous on page 1,
next on the last page) is serialized as null rather than dropped, so the shape does not change per page;
current echoes the request and is always there. Omit the HttpRequest and Links is null as a whole. For
the RFC 8288 header
as well (a null Links writes no header):
var page = await db.Products.PaginateAsync<Product, ProductDto>(request, config, this.Request, ct);
this.Response.AddPaginationLinkHeader(page.Links);
What the client gets back
{
"items": [ { "id": "7f3c…", "name": "Widget Pro", "price": 249.00 } ],
"meta": { "totalItems": 26, "itemCount": 1, "itemsPerPage": 25, "totalPages": 2, "currentPage": 2,
"sortBy": ["name:ASC"], "search": null, "searchBy": [],
"filter": { "status": ["$eq:Active"] },
"hasPreviousPage": true, "hasNextPage": false },
"links": { "first": "/products?limit=25&filter.status=%24eq%3AActive&page=1",
"previous": "/products?limit=25&filter.status=%24eq%3AActive&page=1",
"next": null,
"last": "/products?limit=25&filter.status=%24eq%3AActive&page=2",
"current": "/products?limit=25&filter.status=%24eq%3AActive&page=2" }
}
meta echoes the effective request, not the raw one: this response reports "sortBy": ["name:ASC"] even
though the client sent no sortBy, because that is where DefaultSortBy landed — which is exactly what a
grid header needs to draw its arrow. Every key is always present; null, [] and {} carry the absent
cases. See
Response contract.
Errors
Any invalid query becomes 400 Bad Request with title: "Invalid query" and the
specific message as detail — via PaginateExceptionFilter for controllers (registered by AddAspNetCore()) or
PaginateExceptionEndpointFilter for Minimal APIs (registered by WithPagination<T>()). Both answer
application/problem+json and each is enriched exactly once by its own framework half, so an
AddProblemDetails customisation applies on either. The members match where the framework halves do:
type, title, status, detail and code come from this library on both legs, while traceId is the
framework's and reaches a Minimal API response only when the app registered AddProblemDetails(). No per-action try/catch needed.
The payload carries a code member — the PaginateQueryException.Code member name, such as
SortFieldNotConfigured — so a client can branch on the cause instead of matching the detail prose.
Unknown query parameters are ignored, so clients keep their own tracking parameters; page and limit are
validated.
Documentation
- ASP.NET Core integration
- OpenAPI
- Getting started
- Query-string contract
- Response contract
- Errors
- Full guide
Debugging
The package ships embedded PDBs with Source Link, so a debugger steps straight into these sources at the exact commit the version was built from. Nothing to configure: no symbol server, no separate symbol download, and it works offline.
License
MIT © Lubos Jansky
| 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
- Janzen.Pagination.EntityFrameworkCore (>= 10.1.0)
- Microsoft.AspNetCore.OpenApi (>= 10.0.12)
- Microsoft.EntityFrameworkCore (>= 10.0.12)
- Microsoft.OpenApi (>= 2.12.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.