Repository navigation
Recipes
Practical, end-to-end snippets for the most frequent scenarios. Each recipe is self-contained and assumes you have already called AddPagedListResultWeb (and AddPagedListResultApiExplorer for OpenAPI).
public sealed record ListProductsQuery(PageRequestWithFilters Request)
: IRequest<IPagedResult<ProductDto>>;
public sealed class ListProductsHandler
: IRequestHandler<ListProductsQuery, IPagedResult<ProductDto>>
{
private readonly AppDbContext _db;
public ListProductsHandler(AppDbContext db) => _db = db;
public async Task<IPagedResult<ProductDto>> Handle(
ListProductsQuery q, CancellationToken ct)
{
var page = await _db.Products
.AsNoTracking()
.GetPagedWithFiltersAsync(q.Request, cancellationToken: ct);
return page.Map(page.Response.Select(p => new ProductDto(p.Id, p.Name)));
}
}The Map<TDestination> extension on PagedResult<T> clones the envelope (PageCount, RowCount, ExecutionDetails, ...) and swaps the items. No additional plumbing is needed.
[HttpPost]
public async Task<IActionResult> List(
[FromBody] PageRequestWithFilters request, CancellationToken ct)
{
request.PredefinedRecord = new DataPredefinedFilterDefinition
{
PredefinedFieldName = "Id",
PredefinedRecords = new[] { "1", "2", "3" }
};
var paged = await _db.Products.AsNoTracking()
.GetPagedWithFiltersAsync(
request,
new DefaultPrimaryKeyDefinition("Id"),
cancellationToken: ct);
return PagedOkResult(paged);
}Items with Id IN (1, 2, 3) are pinned to page 1; the remaining slots of the first page (and all subsequent pages) come from the regular paged query.
// Program.cs
builder.Services
.AddPagedListResultWeb()
.ConfigurePageable<Product>(p =>
{
p.AllowSort("Id", "Name", "Price", "CreatedOn");
p.AllowFilter("Status", "Price", "CreatedOn");
p.AllowSearch("Name", "Description");
});
// Controller
[HttpGet]
public async Task<IActionResult> Search(
[FromPagedQuery(typeof(Product))] PageRequestWithFilters request,
CancellationToken ct)
{
if (!ModelState.IsValid)
return ValidationProblem(ModelState);
var paged = await _db.Products.AsNoTracking()
.GetPagedWithFiltersAsync(request, cancellationToken: ct);
return PagedOkResult(paged);
}Sample request:
GET /api/products/search
?page=1&pageSize=20
&order=price:desc
&search=usb
&filter=status:Equals:active
&filter=price:Between:10,100
app.MapGet("/products",
async (PagedQueryWithFilters<Product> q, AppDbContext db, HttpContext ctx, CancellationToken ct) =>
{
if (!q.IsValid)
return Results.ValidationProblem(
q.Errors.ToDictionary(e => e.Key, e => new[] { e.Value }));
var paged = await db.Products.AsNoTracking()
.GetPagedWithFiltersAsync(q, cancellationToken: ct);
return paged.ToPagedHttpResult(ctx);
})
.WithPagedResult();[HttpPost]
[Produces("text/xml")]
public async Task<IActionResult> ListXml(
[FromBody] PageRequestWithFilters request, CancellationToken ct)
{
var paged = await _db.Products.AsNoTracking()
.GetPagedWithFiltersAsync(request, cancellationToken: ct);
return PagedXmlResult(paged);
}The base controller serializes the result through ToSoapXmlPagedResult() and returns a text/xml payload that round-trips with XmlSerializer. The failure path still emits application/problem+json.
builder.Services.AddPagedListResultWeb(o =>
{
o.StatusCodeMapper = (result, defaultCode) =>
{
if (result.Messages?.Any(m => m.Code == "FORBIDDEN") == true)
return StatusCodes.Status403Forbidden;
return defaultCode;
};
});The mapper is invoked by the upstream IProblemDetailsResultFactory and lets you reshape the failure HTTP code per result envelope.
public sealed class Product
{
[PaginationDefaultTopRecordPrimaryKey]
public Guid Id { get; set; }
[PaginationDefaultOrderProperty]
public string Name { get; set; }
public decimal Price { get; set; }
}
// request
new PageRequestWithFilters
{
Page = 1, PageSize = 20,
Order = new DataOrderDefinition { OrderByDefaultProperty = true },
PredefinedRecord = new DataPredefinedFilterDefinition
{
PredefinedRecords = new[] { "fa1...", "fa2..." }
}
}With the attributes in place you do not need to pass a DefaultPrimaryKeyDefinition; the EF Core extension resolves both the order property and the predefined-records property automatically.
public sealed class MyProblemFactory : IProblemDetailsResultFactory
{
public ProblemDetails Build(IResult result, int statusCode)
=> new ProblemDetails
{
Type = "https://errors.example.com/" + statusCode,
Title = "Request failed",
Status = statusCode,
Detail = string.Join("; ", result.Messages.Select(m => m.Message))
};
}
// Program.cs
services.AddSingleton<IProblemDetailsResultFactory, MyProblemFactory>();
services.AddPagedListResultWeb();Both PagedOkResult and ToPagedHttpResult route failures through this factory, so the customization is honored across MVC and minimal APIs alike.
var request = new PageRequestWithFilters();
PagedRequestQueryParser.Populate(
request,
queryCollection,
new PagedListResultWebOptions { MaxPageSize = 200 },
allowList: registry.Get(typeof(Product)));Use this in queue workers, scheduled jobs or other non-HTTP entry points where you still want to honor the same query grammar.
app.MapGet("/_diag/pageable",
(IPageableMetadataRegistry r) => Results.Ok(
r.All.Select(m => new
{
type = m.Type.FullName,
sort = m.AllowedSort,
filter = m.AllowedFilter,
search = m.AllowedSearch
})));Useful for integration tests and for documenting your API to consumers without browsing the source code.
As of v5, the query wrappers PagedQuery<T> / PagedQueryWithFilters<T> (namespace RzR.ResultMessage.Pagination.AspNetCore.Query) are dual-host: besides self-binding in Minimal API, they also bind in MVC when used as [FromPagedQuery] action parameters. Because they implement IPagedQueryValidation, you can inspect request.IsValid / request.Errors directly on the bound instance — handy on a controller that is not decorated with [ApiController] (and therefore has no automatic 400 on invalid model state).
// Program.cs
builder.Services
.AddPagedListResultWeb()
.ConfigurePageable<Product>(p =>
{
p.AllowSort("Id", "Name", "Price", "CreatedOn");
p.AllowFilter("Status", "Price", "CreatedOn");
p.AllowSearch("Name", "Description");
});
// Controller
[HttpGet("paged")]
public async Task<IActionResult> Paged(
[FromPagedQuery(typeof(Product))] PagedQueryWithFilters<Product> request,
CancellationToken ct)
{
if (!request.IsValid)
return ValidationProblem(
new ValidationProblemDetails(
request.Errors.ToDictionary(e => e.Key, e => new[] { e.Value })));
var paged = await _db.Products.AsNoTracking()
.GetPagedWithFiltersAsync(request, cancellationToken: ct);
return PagedOkResult(paged);
}Prefer the plain PageRequestWithFilters parameter (Recipe 3) under [ApiController], where invalid binding already yields an automatic 400. Reach for the PagedQueryWithFilters<T> wrapper when you want to own the validation branch yourself — the same wrapper type then works unchanged in a Minimal API endpoint (Recipe 4).
PagedListResult is maintained by RzR. GitHub repository.
Start here
Reference
- Data Models Reference
- Expression Builders (Common)
- EntityFramework Core Extensions
- Query String Syntax
ASP.NET Core integration
- Web Integration Overview
- MVC Integration
- Minimal API Integration
- Configuration and Options
- Allow List Registry
- Response Headers and ProblemDetails
- OpenAPI and ApiExplorer
Operations