Skip to content
FlexQuery.NET

Query Syntax

FlexQuery accepts three query languages on the same endpoint. All three parse into the same internal QueryOptions model, and DSL and FQL expose the full feature set — filtering, sorting, projection, include/expand, grouping, aggregates, paging — interchangeably. MiniOData is intentionally a lighter compatibility layer: it covers filter, sort, select, and relationship loading (its $expand maps to plain include), while grouping, aggregates, filtered expansion, and keyset paging remain DSL/FQL features. The syntax is a client-facing choice, not a server-side fork.

SyntaxEnum valueStyleExtra package
Native DSLQuerySyntax.NativeDslfilter=Status:eq:Activebuilt-in
FQLQuerySyntax.Fqlfilter=Status = 'Active'FlexQuery.NET.Parsers.Fql
MiniODataQuerySyntax.MiniOData$filter=Status eq 'Active'FlexQuery.NET.Parsers.MiniOData

Choosing a syntax: DSL is compact and URL-friendly — the default. FQL reads like SQL and suits developer-facing tools. MiniOData eases migration from OData consumers.

Selecting the syntax

Globally

C#
FlexQueryCore.Configure(options =>
{
    options.DefaultQuerySyntax = QuerySyntax.Fql;
});

Per request

C#
var result = await db.Customers.FlexQueryAsync(
    parameters,
    opt => opt.QuerySyntax = QuerySyntax.MiniOData,
    cancellationToken: cancellationToken);

Registering parsers

The DSL parser is built in. FQL and MiniOData live in separate packages and must be registered once at startup:

C#
using FlexQuery.NET.Parsers.Fql;
using FlexQuery.NET.Parsers.MiniOData;

Fql.Register();
MiniOData.Register();

Registration must happen before any execution; requesting an unregistered syntax throws ParserNotRegisteredException.

Parameter map

DSL and FQL share the same parameter keys; MiniOData uses its $-prefixed spellings for the expressions it supports.

ParameterDSL exampleFQL exampleMiniOData
Filterfilter=Status:eq:Activefilter=Status = 'Active'$filter=Status eq 'Active'
Sortsort=Name:asc,Age:descsort=Name ASC, Age DESC$orderby=Name asc, Age desc
Selectselect=Id,Name,Orders.Totalselect=Id,Name$select=Id,Name
Includeinclude=Ordersinclude=Orders$expand=Orders
GroupBygroupBy=StatusgroupBy=Status
Aggregateaggregate=sum:Total:TotalRevenueaggregate=SUM(Total) AS TotalRevenue
Havinghaving=sum:Total:gt:100having=SUM(Total) > 100
Expand (filtered)expand=Orders(filter=Status:eq:'Active'; sort=OrderDate:desc; take=5)same options shape, FQL expressions inside (filter/sort/take)
Page / PageSizepage=1&pageSize=20same$top / $skip (translated to page/size)
Distinctdistinct=truesame
Modemode=flatsame
Cursor / keysetuseKeysetPagination=true&cursor=...same

DSL filter grammar

Plain text
filter    = condition ((AND|OR) condition)*
condition = field:operator:value
field     = property.path (dot-separated navigation)
value     = literal (unquoted single token or 'quoted string')
  • Logical operators: the AND / OR keywords and the symbolic & / | forms are both accepted; AND has higher precedence than OR (the parser builds AND-groups inside OR-groups).
  • Collection operators (any, all, count) target collection navigations: Orders:any:Total:gt:100.
  • Values containing spaces or reserved keywords must be quoted: City:eq:'New York'.
  • The keywords AND/OR are reserved — an unquoted value that starts with one is rejected with a hint to quote it (name:eq:"AND").
  • Null-check operators take no value: DeletedAt:isnull.

FQL filter grammar

Plain text
filter     = condition ((AND|OR) condition)*
condition  = field op value | field [NOT] IN (...) | field [NOT] BETWEEN a AND b
             | field IS [NOT] NULL | field [NOT] LIKE '%pattern%'
op         = = | != | > | >= | < | <= | CONTAINS | STARTSWITH | ENDSWITH
collection = field ANY (…) | field ALL (…)

FQL is SQL-inspired: values are quoted strings (or numbers/booleans), operators are words or symbols, and parentheses group expressions:

Plain text
filter=Status = 'Active' AND (Age >= 18 OR City = 'Berlin')

MiniOData filter grammar

Plain text
filter    = comparison ((and|or) comparison)* | not (filter)
comparison = field op value
            | field [not] in (v1, v2, ...)
            | field is null | field is not null
op        = eq | ne | gt | ge | lt | le
functions = contains(field,'x') | startswith(field,'x') | endswith(field,'x')
paths     = slash-separated: Orders/TotalAmount gt 100
Plain text
$filter=Status eq 'Active' and Age ge 18

The parser is deliberately small: flat paths and the operators above. It does not implement the full OData vocabulary ($apply, nested $expand options, etc.).

Aggregate syntax

DSL aggregates use the aggregate parameter with function:field[:alias] triples; FQL uses FUNCTION(field) [AS alias]:

Plain text
aggregate=sum:Total:TotalRevenue,count:Id          (DSL)
aggregate=SUM(Total) AS TotalRevenue, COUNT(Id)    (FQL)
  • Functions: sum, count, avg (or average), min, max.
  • Without an explicit alias, the output field is the PascalCase field + function (sum:TotalTotalSum).
  • Aggregates combine with groupBy; having references declared aggregates: having=sum:Total:gt:100 (DSL) or having=SUM(Total) > 100 (FQL).

Sort syntax

Both direction spellings are accepted:

Plain text
sort=Name:asc,Age:desc      (colon form)
sort=Name ASC, Age DESC     (space form)

Aggregate sorts use function:target:direction (DSL) or SUM(Field) DESC (FQL) — see Sorting.

Complete worked example

One endpoint, three syntaxes, same result:

C#
[HttpGet("api/customers")]
public async Task<IActionResult> Get(
    [FromQuery] FlexQueryParameters parameters,
    CancellationToken cancellationToken)
{
    var result = await db.Customers
        .AsNoTracking()
        .FlexQueryAsync(parameters, cancellationToken);
    return Ok(result);
}
HTTP
GET /api/customers?filter=Status:eq:Active AND Age:gte:18        (DSL)
GET /api/customers?filter=Status:eq:Active & Age:gte:18          (DSL, symbolic)
GET /api/customers?filter=Status = 'Active' AND Age >= 18        (FQL)
GET /api/customers?$filter=Status eq 'Active' and Age ge 18      (MiniOData)

All four produce identical results.

Common mistakes