ASP.NET Core
FlexQuery.NET.AspNetCore binds FlexQuery to the MVC model-binding and filter pipeline. It
adds three things: DI registration helpers, the [FieldAccess] attribute for per-endpoint
governance, and the result-shape JSON converter that makes select surfaces authoritative
in serialized output.
Setup
builder.Services.AddControllers()
.AddFlexQuerySecurity() // [FieldAccess] filter + result-shape JSON converter
.AddFlexQueryJson(); // result-shape JSON converter onlyAddFlexQuerySecurity registers the FieldAccessFilter (which reads [FieldAccess]
attributes) and the QueryResultShapeConverterFactory. AddFlexQueryJson registers only
the JSON converter. A combined shortcut exists too:
builder.Services.AddFlexQuery(options =>
{
options.CreateMap<Customer, CustomerResponse>();
});This performs FlexQueryCore.Configure(configure) (global defaults and global type maps).
Endpoint pattern
using FlexQuery.NET;
using FlexQuery.NET.Models;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/customers")]
[FieldAccess(Allowed = ["Id", "FirstName", "Email", "Status"], AllowedIncludes = ["Orders"])]
public sealed class CustomersController(AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get(
[FromQuery] FlexQueryParameters parameters,
CancellationToken cancellationToken)
{
var result = await db.Customers
.AsNoTracking()
.FlexQueryAsync(parameters, cancellationToken: cancellationToken);
return Ok(result);
}
}FieldAccessAttribute
Apply on an action (takes priority) or the controller. All properties merge into the request's execution options before validation:
| Property | Purpose |
|---|---|
Allowed | Allow-list of field names. |
Blocked | Blocked field names. |
Filterable | Fields clients may filter on. |
Sortable | Fields clients may sort by. |
Selectable | Fields clients may select. |
Groupable | Fields clients may group by. |
Aggregatable | Fields clients may aggregate. |
AllowedIncludes | Navigation paths clients may include or expand. |
DefaultSortField / DefaultSortDirection | Default ordering when the client does not sort. |
MaxDepth | Maximum nested field-path depth (-1 = unset). |
The filter resolves the attribute with action over controller priority, merges each
list with any already-resolved execution options, and stores the result in
HttpContext.Items.
Reading execution options from HttpContext
The [FieldAccess] filter stores the resolved options on the request; the provider
call itself is driven by the options you pass. The intended pattern is to hand the
attribute's options to FlexQueryAsync — the GetFlexQueryExecutionOptions() extension
reads them back for exactly that purpose:
var execOptions = httpContext.GetFlexQueryExecutionOptions();Custom middleware, authorization checks, or adapters can inspect or extend the same object before execution.
Complete worked example
A locked-down public endpoint wiring [FieldAccess] into execution explicitly:
[ApiController]
[Route("api/public/customers")]
[FieldAccess(
Allowed = ["Id", "City", "Status"],
Sortable = ["Id", "City"],
AllowedIncludes = [],
DefaultSortField = "Id",
MaxDepth = 2)]
public sealed class PublicCustomersController(AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get(
[FromQuery] FlexQueryParameters parameters,
CancellationToken cancellationToken)
{
var execOptions = HttpContext.GetFlexQueryExecutionOptions();
var result = await db.Customers
.AsNoTracking()
.FlexQueryAsync(parameters,
opt =>
{
opt.AllowedFields = execOptions.AllowedFields;
opt.SortableFields = execOptions.SortableFields;
opt.AllowedIncludes = execOptions.AllowedIncludes;
opt.DefaultSortField = execOptions.DefaultSortField;
opt.MaxFieldDepth = execOptions.MaxFieldDepth;
},
cancellationToken: cancellationToken);
return Ok(result);
}
}Requests against this endpoint: filter=City:eq:Berlin works; filter=Email:contains:@
fails validation; include=Orders fails (empty allow-list); pageSize=500 works but the
JSON surface only ever contains Id, City, Status.