Skip to content
FlexQuery.NET

FlexQueryRequest

FlexQueryRequest (FlexQuery.NET.Models) is the strongly-typed request object for FlexQuery.NET. It is designed for POST bodies, typed HTTP clients, and OpenAPI-documented APIs where raw query-string binding is not the right shape. Every property maps directly into the pipeline's canonical QueryOptions through FlexQueryRequestExtensions.ToQueryOptions().

Where it fits in the pipeline

Plain text
FlexQueryRequest (POST body)
        │
        ▼  request.ToQueryOptions()
    QueryOptions
        │
        ▼  FlexQueryAsync(options, configure, ct)
    QueryResult<T>

FlexQueryRequest is a request-layer type. It is not parsed from strings — its structured properties are already AST-shaped objects. The pipeline never sees a FlexQueryRequest directly; providers accept QueryOptions or FlexQueryParameters.

How it differs from the other request models

ModelShapeTypical sourceConsumed by
FlexQueryParametersFlat string properties[FromQuery] GET bindingFlexQueryAsync(parameters, ...)
FlexQueryRequestStructured object propertiesJSON POST bodyrequest.ToQueryOptions() → FlexQueryAsync(options, ...)
QueryOptionsParsed ASTAny of the above, fluent builder, adaptersAll provider FlexQueryAsync overloads

Properties reference

Filter

Type: FilterGroup?
Default: null
Optional: Yes

The root filter expression tree. Accepts the same DSL/FQL structure as QueryOptions.Filter: conditions, nested AND/OR groups, collection predicates (any, all), and all supported operators.

Effect: Produces the WHERE clause. When null, no filter is applied.

JSON
"filter": {
  "logic": "and",
  "filters": [
    { "field": "Status", "operator": "eq", "value": "Active" },
    { "field": "City", "operator": "in", "value": "Berlin,Munich" }
  ]
}

See Filtering for operator syntax.


Sort

Type: List<SortNode>
Default: [] (never null)
Optional: Yes — empty list produces no ORDER BY

Ordered sort specifications. Each SortNode carries Field, Descending, and optional Aggregate/AggregateField for collection sorts.

Effect: Produces ORDER BY. List order is priority order. Providers may inject a default sort when none is supplied to guarantee stable paging.

JSON
"sort": [
  { "field": "LastName", "descending": false },
  { "field": "CreatedAt", "descending": true }
]

See Sorting.


Select

Type: List<string>?
Default: null
Optional: Yes

Flat dot-notation selection paths. Each string is a field path such as "Id" or "Profile.Name". Aliases and nested selections are not represented here — for those use QueryOptions.Select built through the fluent API or parsers.

Effect: When non-null, only the listed fields materialize. When null, the full entity surface is projected.

During ToQueryOptions(), each string becomes a SelectNode with only Field set:

C#
request.Select?.Select(f => new SelectNode { Field = f }).ToList()
JSON
"select": ["Id", "FirstName", "Email", "Profile.Name"]

See Projection.


Include

Type: List<IncludeNode>?
Default: null
Optional: Yes

Navigation relationships to include. Each IncludeNode carries:

MemberTypePurpose
PathstringNavigation path, e.g. "Orders" or "Orders.OrderItems"
FilterFilterGroup?Optional inline filter on the collection
SortList<SortNode>?Optional sort on the collection
Takeint?Optional slice limit on the collection
ChildrenList<IncludeNode>Nested relationship levels

Effect: Attaches related data to each result row. On collection-valued relationships, filter/sort/take become relationship-scoped query operations executed server-side. Single-valued relationships can be included but ignore filter/sort/take.

During ToQueryOptions(), dotted Path values are split into a hierarchical tree by IncludeTree.SplitDottedPaths:

JSON
"include": [
  {
    "path": "Orders",
    "take": 5,
    "filter": {
      "logic": "and",
      "filters": [
        { "field": "Status", "operator": "eq", "value": "Delivered" }
      ]
    },
    "sort": [
      { "field": "OrderDate", "descending": true }
    ]
  },
  {
    "path": "Orders.OrderItems"
  }
]

See Include.


Mode

Type: ProjectionMode
Default: ProjectionMode.Nested
Optional: No — always set; omit to accept the default

Controls how nested selections are shaped in the output.

ValueBehavior
NestedHierarchical objects — root → children → grandchildren
FlatNested collections flatten with SelectMany into a leaf-level rowset
FlatMixedRoot scalars and nested-collection fields share one output row

Effect: Passed through to QueryOptions.ProjectionMode; providers honor it during materialization.

JSON
"mode": "FlatMixed"

See Projection.


GroupBy

Type: List<string>?
Default: null
Optional: Yes

Fields to group by for aggregation. Each string is a property path.

Effect: When non-empty, the query executes through the grouped-query executor. Rows are grouped by the specified fields, and aggregate expressions are evaluated per group. having prunes groups before paging and ordering.

JSON
"groupBy": ["Status", "Region"]

See Grouping & Aggregates.


Aggregate

Type: List<Aggregate>
Default: [] (never null)
Optional: Yes — empty list produces no aggregates

Aggregate projection expressions. Each Aggregate carries:

MemberTypePurpose
FunctionAggregateFunctionSum, Count, Avg, Min, Max
Fieldstring?Field to aggregate; optional for Count
AliasstringOutput property name (defaults to PascalCase of Function + Field)

Effect: Ungrouped aggregates compute grand totals over the filtered set and appear in QueryResult.Aggregates. Grouped aggregates appear per-group inside Data.

JSON
"aggregate": [
  { "function": "Sum", "field": "TotalAmount", "alias": "TotalRevenue" },
  { "function": "Count", "field": "Id", "alias": "OrderCount" }
]

See Grouping & Aggregates.


Having

Type: HavingNode?
Default: null
Optional: Yes — only meaningful when groupBy is also set

HAVING condition tree over aggregate values. References must match aliases declared in aggregate.

Effect: Prunes groups before ordering and paging. Without groupBy, the pipeline rejects the request.

HavingNode is abstract; concrete forms are built through the fluent API or FQL parser. In a FlexQueryRequest built from JSON, construct the appropriate concrete type directly (HavingConditionNode, HavingLogicalNode, etc.).

JSON
"having": {
  "type": "condition",
  "function": "Sum",
  "field": "TotalAmount",
  "operator": "gt",
  "value": 1000
}

See Grouping & Aggregates.


Distinct

Type: bool?
Default: null
Optional: Yes

When true, applies Distinct() to the query before projection.

Effect: Eliminates duplicate rows from the result set.

JSON
"distinct": true

Paging

Type: PagingOptions
Default: new() → Page = 1, PageSize = 20, Disabled = false
Optional: No — always present; accept defaults unless you need to override

Pagination parameters.

MemberTypeDefaultBehavior
Pageint11-based page number; clamped to >= 1
PageSizeint20Items per page; clamped to 1–1000
DisabledboolfalseWhen true, returns the full unfiltered result set
Skipintcomputed(Page - 1) * PageSize — read-only

Effect: Offset paging uses Skip/Take. Keyset paging is activated via UseKeysetPagination on QueryOptions (set through FlexQueryParameters or server-side composition), not through FlexQueryRequest.Paging.

JSON
"paging": {
  "page": 1,
  "pageSize": 20
}

To disable paging:

JSON
"paging": {
  "disabled": true
}

See Paging and Keyset Pagination.


IncludeCount

Type: bool?
Default: true
Optional: Yes

Whether the pipeline computes and returns the total filtered count.

Effect: When true (the default), QueryResult.TotalCount contains the count of rows matching the filter before paging. When false, TotalCount is null.

JSON
"includeCount": true

Complete example

One JSON body exercising the major capabilities:

JSON
{
  "filter": {
    "logic": "and",
    "filters": [
      { "field": "Status", "operator": "eq", "value": "Active" },
      { "field": "Region", "operator": "in", "value": "EMEA,APAC" }
    ]
  },
  "sort": [
    { "field": "LastName", "descending": false },
    { "field": "CreatedAt", "descending": true }
  ],
  "select": ["Id", "FirstName", "LastName", "Email"],
  "include": [
    {
      "path": "Orders",
      "take": 5,
      "filter": {
        "logic": "and",
        "filters": [
          { "field": "Status", "operator": "eq", "value": "Delivered" }
        ]
      },
      "sort": [
        { "field": "OrderDate", "descending": true }
      ]
    },
    {
      "path": "Profile.Address"
    }
  ],
  "mode": "Nested",
  "groupBy": ["Status"],
  "aggregate": [
    { "function": "Sum", "field": "TotalAmount", "alias": "TotalRevenue" },
    { "function": "Count", "field": "Id", "alias": "OrderCount" }
  ],
  "having": {
    "type": "condition",
    "function": "Sum",
    "field": "TotalAmount",
    "operator": "gt",
    "value": 1000
  },
  "distinct": false,
  "paging": {
    "page": 1,
    "pageSize": 20
  },
  "includeCount": true
}

Conversion to QueryOptions

FlexQueryRequestExtensions.ToQueryOptions() produces the QueryOptions the pipeline consumes. The mapping is direct:

FlexQueryRequestQueryOptionsNotes
FilterFilterDirect assignment
SortSortDirect reference assignment
SelectSelectStrings become flat SelectNode leaves (Field only)
IncludeIncludesDotted paths split into a hierarchical tree; options attach to the leaf segment
ModeProjectionModeDirect assignment
GroupByGroupByDirect assignment
AggregateAggregatesDirect reference assignment
HavingHavingDirect assignment
DistinctDistinctDirect assignment
PagingPagingDirect reference assignment
IncludeCountIncludeCountDirect assignment

Execution patterns

FlexQueryRequest is not accepted directly by any provider FlexQueryAsync overload. Convert it first, then execute.

EF Core — dynamic results

C#
using FlexQuery.NET.Models;
using FlexQuery.NET.EntityFrameworkCore;

[HttpPost("search")]
public async Task<IActionResult> PostSearch(
    [FromBody] FlexQueryRequest request,
    CancellationToken cancellationToken)
{
    var options = request.ToQueryOptions();

    var result = await db.Customers
        .AsNoTracking()
        .FlexQueryAsync(
            options,
            opt =>
            {
                opt.AllowedFields = ["Id", "FirstName", "LastName", "Email", "Status"];
                opt.MaxPageSize = 100;
            },
            cancellationToken);

    return Ok(result);
}

Relevant overloads:

C#
Task<QueryResult<object>> FlexQueryAsync<T>(
    this IQueryable<T> query,
    QueryOptions queryOptions,
    Action<EfCoreQueryOptions>? configure = null,
    CancellationToken cancellationToken = default)
C#
Task<QueryResult<object>> FlexQueryAsync<T>(
    this IQueryable<T> query,
    QueryOptions queryOptions,
    EfCoreQueryOptions options,
    CancellationToken cancellationToken = default)

EF Core — typed DTO results

C#
public record CustomerDto(int Id, string Name, string Email);

var result = await db.Customers
    .AsNoTracking()
    .FlexQueryAsync<Customer, CustomerDto>(
        options,
        opt => { opt.AllowedFields = ["Id", "FirstName", "LastName", "Email"]; },
        cancellationToken);

Relevant overloads:

C#
Task<QueryResult<TResponse>> FlexQueryAsync<TEntity, TResponse>(
    this IQueryable<TEntity> query,
    QueryOptions queryOptions,
    Action<EfCoreQueryOptions>? configure = null,
    CancellationToken cancellationToken = default)
C#
Task<QueryResult<TResponse>> FlexQueryAsync<TEntity, TResponse>(
    this IQueryable<TEntity> query,
    QueryOptions queryOptions,
    EfCoreQueryOptions options,
    CancellationToken cancellationToken = default)

Dapper — dynamic results

C#
using FlexQuery.NET.Dapper;

[HttpPost("search")]
public async Task<IActionResult> PostSearch(
    [FromBody] FlexQueryRequest request,
    [FromServices] IDbConnectionProvider connections,
    CancellationToken cancellationToken)
{
    await using var connection = connections.Open();
    var options = request.ToQueryOptions();

    var result = await connection.FlexQueryAsync<Customer>(
        options,
        opt =>
        {
            opt.AllowedFields = ["Id", "FirstName", "LastName", "Email", "City", "Status"];
            opt.MaxPageSize = 200;
        },
        cancellationToken: cancellationToken);

    return Ok(result);
}

Relevant overloads:

C#
Task<QueryResult<object>> FlexQueryAsync<T>(
    this DbConnection connection,
    QueryOptions queryOptions,
    DapperQueryOptions? options = null,
    CancellationToken cancellationToken = default)

Dapper — typed DTO results

C#
public record CustomerDto(int Id, string Name, string Email);

var result = await connection.FlexQueryAsync<Customer, CustomerDto>(
    options,
    opt => { opt.AllowedFields = ["Id", "FirstName", "LastName", "Email"]; },
    cancellationToken: cancellationToken);

Relevant overloads:

C#
Task<QueryResult<TResponse>> FlexQueryAsync<TEntity, TResponse>(
    this DbConnection connection,
    QueryOptions queryOptions,
    Action<DapperQueryOptions>? configure = null,
    CancellationToken cancellationToken = default)
C#
Task<QueryResult<TResponse>> FlexQueryAsync<TEntity, TResponse>(
    this DbConnection connection,
    QueryOptions queryOptions,
    DapperQueryOptions options,
    CancellationToken cancellationToken = default)

Manual pipeline

When you need stages individually:

C#
var options = request.ToQueryOptions();
options.ValidateOrThrow<Customer>(execOptions);

var query = db.Customers.AsQueryable();
query = query.ApplyFilter(options);
query = query.ApplySort(options);
query = query.ApplyPaging(options);
query = query.ApplyIncludes(options);

var data = await query.ApplySelect(options).ToListAsync(cancellationToken);
return Ok(options.BuildQueryResult(data, total));

See Query Composition.

Parsed properties vs. execution settings

Property categoryPropertiesNotes
Parsed query expressionsFilter, Sort, Select, Include, GroupBy, Aggregate, HavingThese are query semantics — they shape the SQL/query itself.
Execution / result settingsPaging, IncludeCount, Distinct, ModeThese control how the executed result is shaped or whether it is computed.
Not a request propertyCursor, UseKeysetPaginationKeyset pagination lives on FlexQueryParameters and QueryOptions, not on FlexQueryRequest. Set them server-side when needed.

Governance settings (AllowedFields, BlockedFields, AllowedIncludes, etc.) are execution-time configuration, not request properties. Pass them through the configure delegate or EfCoreQueryOptions/DapperQueryOptions objects.