Query Result
Every execution method returns QueryResult<T> — a uniform envelope that pairs the page of
data with the metadata clients need for pagination UIs, aggregate displays, and cursor-based
navigation. Because the envelope is the same for EF Core and Dapper, dynamic and typed
results, your response contract never changes when the query does.
Properties
| Property | Type | Description |
|---|---|---|
Data | IReadOnlyList<T> | The page of results (entities, projected objects, or DTOs). |
TotalCount | int? | Source rows matching the query before paging — independent of what is returned in Data. Null when counting is disabled. On grouped queries it is the number of underlying rows, not the number of groups. |
ResultCount | int? | The post-shaping row total (groups for grouped queries, distinct rows for distinct) and what TotalPages is computed from. Null on plain queries unless the provider computes it. |
Page | int | Current 1-based page number. |
PageSize | int | Effective page size (after clamping). |
TotalPages | int | Computed from total count and page size. |
HasNextPage | bool | A next page exists. |
HasPreviousPage | bool | A previous page exists. |
Aggregates | Dictionary<string, Dictionary<string, object>>? | Grand totals for ungrouped aggregate queries: field → aggregate key → value. Null otherwise. |
NextCursorToken | string? | Cursor for the next keyset page (keyset mode only). |
ResultShape | IReadOnlyList<SelectOutputField>? | The effective output surface when an explicit select is present. |
ResultShape fields
Each SelectOutputField describes one output column:
| Field | Meaning |
|---|---|
SourceName | The public/source field the client requested (e.g. CustomerFullName). |
SourcePropertyName | The entity property it resolves to. |
OutputName | The response field name — the alias when present, otherwise the source name. |
When the result-shape JSON converter is registered (AddFlexQuerySecurity() /
AddFlexQueryJson()), serialization enforces exactly this surface: anything outside it is
stripped from the payload, with aliases applied.
Serialization example
{
"data": [ /* 20 rows */ ],
"totalCount": 137,
"page": 3,
"pageSize": 20,
"totalPages": 7,
"hasNextPage": true,
"hasPreviousPage": true
}(The resultCount key is only present when a result-shape count was computed — grouped
or distinct queries.)
Clients can build complete pagination UIs from this envelope alone: page numbers
(totalPages), next/previous buttons (hasNextPage/hasPreviousPage), and row counts
(totalCount).
Grouped queries
Grouped queries produce one Data entry per group. Each row carries the group key(s)
plus every aggregate under its alias, and the paging metadata is computed over groups:
totalCount remains the underlying source-row count, resultCount is the number of
groups, and totalPages/hasNextPage follow from the group count.
{
"data": [
{ "status": "Active", "sumTotal": 1250.00, "countOrders": 14 }
],
"totalCount": 417,
"resultCount": 1,
"page": 1,
"pageSize": 20,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
}Ungrouped aggregate queries (?aggregate=... without groupBy) do not produce group rows;
their single-row totals appear in a separate aggregates object keyed by the aggregate's
source field (or "all"), with inner entries keyed by alias:
{
"data": [ ...paged matching records... ],
"aggregates": { "TotalAmount": { "sumRevenue": 1250.00 } },
"totalCount": 417,
"page": 1,
"pageSize": 20
}See Grouping & Aggregates for the full semantics.
Keyset pagination
When keyset mode is active, NextCursorToken carries the opaque, versioned cursor built
from the sort-key values of the last row on the page. When a page comes back empty, the
token is null — the standard end-of-scroll signal. Pass it back as the cursor
parameter — see Keyset Pagination.
Complete worked example
Shaping a stable public response from the envelope:
var result = await db.Customers
.FlexQueryAsync(parameters, cancellationToken: cancellationToken);
return Ok(new
{
items = result.Data,
pagination = new
{
page = result.Page,
pageSize = result.PageSize,
total = result.TotalCount,
totalPages = result.TotalPages,
},
nextCursor = result.NextCursorToken,
});