Repository navigation
MVC Integration
This page covers the controller-side surface of PagedListResult.Web. For the minimal-API counterpart see Minimal API Integration.
A convenience base class that derives from RzR.ResultMessage.Web.ResultBaseApiController. Inheriting from it gives you four helpers:
| Method | Behavior |
|---|---|
PagedOkResult<T>(IPagedResult<T> response) |
On success returns Ok(response) (status 200, JSON envelope). On failure returns response.AsProblemDetails(HttpStatusCode.BadRequest). |
PagedXmlResult<T>(IPagedResult<T> response) |
On success serializes the result through ToSoapXmlPagedResult() and returns it as Content("...", "text/xml"). On failure returns the same ProblemDetails as above. |
JsonResult<T>(IPagedResult<T> response) |
[Obsolete("Use PagedOkResult instead.")] thin wrapper retained for one release for backward compatibility. |
XmlResult<T>(IPagedResult<T> response) |
[Obsolete("Use PagedXmlResult instead.")] thin wrapper retained for one release. |
The two obsolete wrappers will be removed in a future major release. See Migration Guide for the rename.
The class is decorated with [ApiController] upstream; you do not need to add it again.
[Produces("application/json")]
[Route("api/[controller]/[action]")]
public sealed class ProductsController : BaseApiPagedResultController
{
private readonly AppDbContext _db;
public ProductsController(AppDbContext db) => _db = db;
[HttpPost]
public async Task<IActionResult> List(
[FromBody] PageRequestWithFilters request, CancellationToken ct)
{
IPagedResult<Product> result = await _db.Products
.AsNoTracking()
.GetPagedWithFiltersAsync(request, cancellationToken: ct);
return PagedOkResult(result);
}
}Use this attribute on an action parameter when you want a PagedRequest-derived model to be bound from the query string instead of the body. The attribute lives in RzR.ResultMessage.Pagination.AspNetCore.Attributes and implements IBindingSourceMetadata so MVC routes the parameter through the model binder for BindingSource.Query.
| Constructor | Purpose |
|---|---|
[FromPagedQuery] |
Bind without an entity type. The allow-list lookup is skipped; every query-string field is accepted. |
[FromPagedQuery(typeof(Product))] |
Bind and constrain by the PageableMetadata registered for Product. |
The attribute exposes a single EntityType property (settable via the constructor or by attribute syntax: [FromPagedQuery(EntityType = typeof(Product))]).
The full grammar of the supported query string is documented in Query String Syntax. A short example:
GET /api/products/list
?page=2
&pageSize=20
&order=price:desc
&search=usb
&filter=status:Equals:active
&filter=price:Between:10,100
&fields=Id,Name,Price
&predefinedField=Id
&predefinedRecords=1,2,3
[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);
}PagedRequestModelBinder writes per-row binding errors as filter[i] keys in ModelStateDictionary (where i is the zero-based index of the offending filter in the query string). Returning ValidationProblem(ModelState) produces the standard ASP.NET Core 400 ProblemDetails response carrying those error keys.
As of v5, the minimal-API query wrappers PagedQuery<T> and PagedQueryWithFilters<T> (namespace RzR.ResultMessage.Pagination.AspNetCore.Query) are dual-host. Alongside their BindAsync(HttpContext) support for minimal APIs (see Minimal API Integration), they can now also be bound as an MVC action parameter decorated with [FromPagedQuery]. Both types implement IPagedQueryValidation, which exposes IsValid and Errors.
For most MVC controllers, keep using the plain PagedRequest / PageRequestWithFilters types shown above — per the types' own XML docs this remains the preferred MVC parameter shape, and it plays best with [ApiController]'s automatic ModelState validation and 400 response.
The wrapper is a niche option for controllers without [ApiController] (no automatic 400 on an invalid ModelState), where you want to inspect request.IsValid / request.Errors yourself instead of relying on ModelState:
[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);
}AddPagedListResultWeb configures MvcOptions with three additions:
-
PagedResultApplicationModelConvention-> auto-attaches OpenAPI annotations. -
ServiceFilterAttribute(typeof(PagedResultActionFilter))-> emits the response headers around every action. -
PagedRequestModelBinderProviderinserted at index 0 ofModelBinderProviders-> wins over the default complex-object binder when[FromPagedQuery]is present.
The provider is intentionally narrow:
if (!context.Metadata.IsComplexType) return null;
if (context.BindingInfo.BindingSource != BindingSource.Query) return null;
if (context.BindingInfo.PropertyFilterProvider != null) return null;
if (!typeof(PagedRequest).IsAssignableFrom(modelType)) return null;
return new BinderTypeModelBinder(typeof(PagedRequestModelBinder));This means non-paged complex types and body-bound parameters are not affected.
Registered as a [ServiceFilter] by AddPagedListResultWeb. It runs after the action and:
- Inspects
context.Result. If it isObjectResultorJsonResult, extractsValue. - Calls
PagedResponseHeaderWriter.TryApply(context.HttpContext, value, options). The writer recognizes anyIPagedResult(also through reflection on minimal-API envelopes) and emits the configured headers. - Continues the pipeline with
await next().
Headers do not change the body. If you replace the action result with something that does not carry an IPagedResult, the filter is a no-op.
By default success returns 200 OK and failure returns 400 Bad Request. Override the failure code globally via the options:
builder.Services.AddPagedListResultWeb(o =>
{
o.StatusCodeMapper = (result, defaultCode) =>
{
// Inspect result.Messages and choose a different code if applicable.
return defaultCode;
};
});The mapper is used by IProblemDetailsResultFactory.AsProblemDetails(...) from the upstream RzR.ResultMessage.Web package.
-
[FromPagedQuery]is not active. Verify you registered the binder withAddPagedListResultWeb(...). Without that call the binder provider is not inMvcOptionsand the default complex-object binder takes over. -
ModelStateis empty even though the query is wrong. Make sure the action signature carries thePagedRequest-derived parameter directly. The binder writes errors against the parameter'sModelStateDictionary, which is only created when the binder is invoked. -
JSON envelope is missing on success. Confirm you returned
PagedOkResult(...)and notOk(...)followed by manual shaping. The base controller method also callsIProblemDetailsResultFactoryfor failures, whichOkdoes not. -
Headers are missing. Verify the action returns
ObjectResultorJsonResult(whichPagedOkResultdoes). If you wrap the result in a customIActionResult, also callPagedResponseHeaderWriter.TryApply(...)from inside it or useWithPagedResultstyle filters.
End-to-end controller examples covering MediatR handlers, predefined records and SOAP/XML are in Recipes.
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