Skip to content
FlexQuery.NET

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

C#
builder.Services.AddControllers()
    .AddFlexQuerySecurity()   // [FieldAccess] filter + result-shape JSON converter
    .AddFlexQueryJson();      // result-shape JSON converter only

AddFlexQuerySecurity registers the FieldAccessFilter (which reads [FieldAccess] attributes) and the QueryResultShapeConverterFactory. AddFlexQueryJson registers only the JSON converter. A combined shortcut exists too:

C#
builder.Services.AddFlexQuery(options =>
{
    options.CreateMap<Customer, CustomerResponse>();
});

This performs FlexQueryCore.Configure(configure) (global defaults and global type maps).

Endpoint pattern

C#
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:

PropertyPurpose
AllowedAllow-list of field names.
BlockedBlocked field names.
FilterableFields clients may filter on.
SortableFields clients may sort by.
SelectableFields clients may select.
GroupableFields clients may group by.
AggregatableFields clients may aggregate.
AllowedIncludesNavigation paths clients may include or expand.
DefaultSortField / DefaultSortDirectionDefault ordering when the client does not sort.
MaxDepthMaximum 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:

C#
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:

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

Common mistakes