Your first request¶
With a [GenerateFilter<User>] partial declared and services.AddFiltering() wired into DI, you have everything you need to accept a FilterRequest from an HTTP client and return a PageResult<User>.
The controller endpoint¶
The EF Core helpers package adds IQueryable<T>.ApplyPagedAsync(...), which validates the request, applies filter + sort, runs CountAsync + ToListAsync against the provider, and packages the result into a PageResult<T>:
[HttpPost("search")]
public async Task<ActionResult<PageResult<User>>> Search(
[FromBody] FilterRequest request,
[FromServices] IFilterDefinition<User> userFilter,
[FromServices] AppDbContext dbContext,
CancellationToken cancellationToken)
{
try
{
var page = await dbContext.Users
.ApplyPagedAsync(userFilter, request, cancellationToken);
return Ok(page);
}
catch (FilterValidationException invalid)
{
return BadRequest(invalid.Result);
}
}
IFilterDefinition<User> is the generated singleton; ASP.NET Core resolves it via [FromServices]. The same definition can be injected into any handler — it is stateless and thread-safe.
The request body¶
Clients post a FilterRequest as JSON. The where field is a polymorphic FilterNode — a tree of FilterGroup (with and or or) and FilterLeaf (with field + op + value) nodes:
{
"where": {
"and": [
{ "field": "Name", "op": "contains", "value": "ali" },
{ "field": "IsActive", "op": "eq", "value": true }
]
},
"sort": [{ "field": "Age", "dir": 1 }],
"page": 1,
"pageSize": 25
}
The sort array carries { field, dir } items where dir is 0 for ascending and 1 for descending. page is 1-based; pageSize is bounded by the filter's [PageSettings(MaxPageSize = ...)] attribute when present.
What happens on validation failure¶
Apply and ApplyPagedAsync always run validation before they touch the underlying IQueryable<T>. On any validation error, they throw FilterValidationException whose Result property carries a FilterValidationResult — a structured list of FilterValidationError items with codes and JSON-pointer-style paths.
The controller above catches the exception and returns the result as the HTTP 400 body. Clients then see a typed error payload like:
{
"errors": [
{
"code": "OperatorNotAllowed",
"path": "/where/and/0/operator",
"message": "Operator 'fuzzy' is not allowed on field 'Name'."
}
]
}
See the handling validation errors guide for ProblemDetails mapping and middleware patterns.