Skip to content
FlexQuery.NET

Recipes

Practical patterns assembled from the building blocks in the guides. Each recipe states the problem, the approach, and the working code.

Cursor-driven infinite scroll

Problem: a feed or list that clients scroll through indefinitely; offset pages get slower and rows shift between requests.

Approach: keyset pagination with a deterministic sort (unique trailing Id):

HTTP
GET /api/feed?useKeysetPagination=true&pageSize=30&sort=CreatedAt:desc,Id:desc
C#
var result = await db.Posts.FlexQueryAsync(parameters, cancellationToken: cancellationToken);

return Ok(new
{
    items = result.Data,
    nextCursor = result.NextCursorToken,   // null = end of feed
});

The client appends cursor=<nextCursor> to the next request.

Role-based field visibility

Problem: admins see more fields than support staff on the same endpoint.

Approach: role-mapped field sets resolved from the principal:

C#
var result = await db.Employees.FlexQueryAsync(parameters, opt =>
{
    opt.RoleAllowedFields = new()
    {
        ["admin"] = ["Id", "Name", "Email", "Salary"],
        ["support"] = ["Id", "Name", "Email"],
    };
    opt.CurrentRole = user.IsInRole("admin") ? "admin" : "support";
}, cancellationToken);

Faceted dashboard with AG Grid

Problem: an analytics grid where users group by a column and see sums/counts per group, with server-side paging.

Approach: AG Grid SSRM's row-group/value columns map directly to grouping and aggregates — no custom code:

C#
[HttpPost("api/aggrid/orders")]
public async Task<IActionResult> Rows([FromBody] AgGridRequest request, CancellationToken ct)
{
    var options = request.ToQueryOptions();   // rowGroupCols -> groupBy, valueCols -> aggregate

    var result = await db.Orders.FlexQueryAsync(options, cancellationToken: ct);

    return Ok(result.ToAgGridServerSideResponse(request));
}

Export endpoint (no paging)

Problem: an export job needs the full result set.

Approach: disable paging, skip the count query, and cap upstream:

C#
var scoped = db.Orders.Where(o => o.CreatedAt >= since);   // upstream cap

var result = await scoped.FlexQueryAsync(parameters, opt =>
{
    opt.DisablePaging = true;
    opt.IncludeTotalCount = false;   // skip the count query - export does not need it
}, cancellationToken);

Search endpoint with contains + sort

Problem: free-text search across name and email, ranked alphabetically.

Approach: OR'd substring filters with a deterministic sort:

HTTP
GET /api/customers?filter=Name:contains:ana|Email:contains:ana&sort=Name:asc,Id:asc
C#
var result = await db.Customers
    .FlexQueryAsync(parameters, opt =>
        opt.FilterableFields = ["Name", "Email"], cancellationToken);

Per-tenant data isolation

Problem: every query must be scoped to the caller's tenant, no matter what the client asks for.

Approach: wrap the queryable before FlexQuery sees it — FlexQuery governs fields, you govern rows:

C#
var scoped = db.Orders.Where(o => o.TenantId == tenantId);
var result = await scoped.FlexQueryAsync(parameters, cancellationToken: cancellationToken);

Public API with a strict surface

Problem: a public read-only endpoint exposing exactly three fields, no includes, no deep paths.

Approach: [FieldAccess] declares the whole contract on the controller:

C#
[FieldAccess(
    Allowed = ["Id", "City", "Status"],
    Sortable = ["Id", "City"],
    AllowedIncludes = [],
    DefaultSortField = "Id",
    MaxDepth = 2)]

DTO-shaped responses for mobile clients

Problem: mobile clients need small payloads with domain vocabulary names.

Approach: typed DTO projection with a per-query map:

C#
var result = await db.Customers
    .FlexQueryAsync<Customer, CustomerSlimDto>(parameters,
        opt => opt.CreateMap<Customer, CustomerSlimDto>()
            .ForMember(dto => dto.Name, entity => entity.FirstName),
        cancellationToken);

Multi-step wizard state via cursors

Problem: a multi-page wizard needs to resume mid-result-set.

Approach: pass the cursor through the wizard's state; keyset pages are stable because they are key-based, not offset-based:

HTTP
GET /api/customers?useKeysetPagination=true&pageSize=10&sort=Id:asc&cursor=<state.cursor>