Skip to content

Recipes

RzR edited this page Jul 4, 2026 · 2 revisions

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).

1. MediatR handler that returns a paged result

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.

2. Body-bound MVC action with predefined records

[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.

3. Query-string GET endpoint with allow-list

// 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

4. Minimal-API endpoint with full pipeline

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();

5. SOAP/XML response

[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.

6. Custom failure status code

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.

7. Pinning rows from a [PaginationDefaultTopRecordPrimaryKey] attribute

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.

8. Custom ProblemDetails factory

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.

9. Manual query-string parsing (background worker)

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.

10. Diagnostic endpoint that exposes the allow-list

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.

11. MVC allow-listed GET using PagedQueryWithFilters<T>

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).

Clone this wiki locally