Execution Pipeline
Every FlexQuery call — whether it started as a query string, a fluent build, or an adapter request — flows through the same five-stage pipeline. Knowing the stages, their order, and what each one guarantees is what lets you predict result ordering, interpret validation errors, and place custom logic at the right point.
The stages
Query string / request model / fluent build
|
v
+------------+ 1. Parse - parameters -> QueryOptions (syntax-specific parser)
| Parsers |
+------------+
|
v
+------------+ 2. Validate - field existence, operators, types, field access,
| Validation | governance limits, expand paths, aggregates
+------------+
|
v
+------------+ 3. Apply - filter -> grouping/aggregates -> sort -> paging ->
| Builder | projection, composed as expression trees
+------------+
|
v
+------------+ 4. Translate - provider converts expressions to SQL (EF Core)
| Provider | or generates SQL directly (Dapper)
+------------+
|
v
+------------+ 5. Execute - query runs server-side; results materialize
| Execution | into QueryResult<T>
+------------+1. Parse
The syntax selected for the request (global default or per-request override) determines
which parser runs. All parsers produce the same canonical QueryOptions — the rest of the
pipeline is syntax-agnostic. Grammar failures surface as QueryParseException (which
carries the offending parameter name, syntax, received value, and position), with the
syntax-specific parse error as the inner exception — all deriving FlexQueryException.
2. Validate
The rule pipeline checks the parsed options against the entity model and governance configuration. Validation runs before any expression is built, so a rejected request costs no database work. See Validation.
3. Apply
The builder composes LINQ expressions in a fixed order:
- Filter (
WHERE) — narrows rows first; everything downstream operates on fewer rows. - GroupBy / Aggregates / Having — grouping forms after filtering;
HAVINGprunes groups before ordering. - Sort (
ORDER BY) — orders rows (or groups). - Paging (
OFFSET/FETCHor keyset seek predicates) — slices from the ordered set. - Projection (
SELECT) — last, so only requested fields materialize. - Total count — computed on the filtered set, independent of paging and projection.
4. Translate
- EF Core: the composed expression tree hands off to EF's translation — everything
becomes SQL. Include/expand branches use EF Core filtered includes (
.Include(...)expressions withWhere/OrderBy/Takeinside), so the related-data window is applied server-side in EF's own generated SQL. - Dapper: FlexQuery generates the SQL itself — select list (surface-aware, type-map
rewritten), WHERE, GROUP BY/HAVING, ORDER BY, and dialect-specific paging. Related data
loads as separate child queries batched by parent keys (split-query style hydration),
and expand
takebecomes a server-side ranked/limited child query.
5. Execute
The provider executes; results materialize into QueryResult<T> with paging metadata,
optional aggregates, and the optional cursor token. Cancellation is observed across the
async overloads (see provider notes for scope).
Events
Each stage emits an event that any IFlexQueryExecutionListener can observe:
| Hook | Fired when |
|---|---|
QueryParsedAsync | Parameters parsed into QueryOptions. |
QueryTranslatedAsync | Provider translated the query (SQL available). |
QueryExecutedAsync | Database command completed (includes timing). |
QueryMaterializedAsync | Results materialized into the result shape. |
Attaching a listener is a one-liner (opt.Listener = ...), and
FlexQueryDiagnosticsCollector accumulates all four into a report — see
Diagnostics.
Where client code fits
- Before parse — authentication, rate limiting.
- Between parse and execute — governance via the
configuredelegate, tenant scoping by wrapping theIQueryablebefore FlexQuery sees it. - After execute — serialization (result-shape enforcement), diagnostics, response shaping.