Diagnostics & Observability
Dynamic queries are hard to debug precisely because they are dynamic: the SQL that executed
depends on the request. FlexQuery.NET.Diagnostics exposes the pipeline as a stream of
events — parse, translate, execute, materialize — that you can log, collect into a report,
or inspect per stage. When a query misbehaves, the answer is in the events, not in guesses.
Execution events
Implement IFlexQueryExecutionListener (namespace FlexQuery.NET.Execution) to observe the
four pipeline stages. Every method is a ValueTask-returning hook with a default no-op
implementation, so you implement only what you need:
| Hook | Fired when | Contains |
|---|---|---|
QueryParsedAsync(QueryParsedEvent e, CancellationToken ct) | Parameters parsed into QueryOptions | What the client actually asked for |
QueryTranslatedAsync(QueryTranslatedEvent e, ct) | Provider translated the query | Generated SQL / LINQ |
QueryExecutedAsync(QueryExecutedEvent e, ct) | Database command completed | Execution timing and outcome |
QueryMaterializedAsync(QueryMaterializedEvent e, ct) | Results materialized | Result-shape details |
Attach a listener per request through the execution options:
opt.Listener = myListener;Built-in listeners
ConsoleExecutionListener— writes each stage to the console; ideal for development.FlexQueryDiagnosticsCollector— accumulates all events in memory for programmatic inspection.
var collector = new FlexQueryDiagnosticsCollector();
var result = await db.Customers.FlexQueryAsync(parameters, opt => opt.Listener = collector);
FlexQueryDiagnosticsReport report =
collector.BuildReport(provider: "EF Core", translator: "Sqlite");
collector.Clear();BuildReport aggregates the collected events into a FlexQueryDiagnosticsReport covering
all four stages with durations — useful for request-scoped debug endpoints (the sample
application wraps this in a DiagnosticsHelper that attaches a __diagnostics object to
responses during development).
SQL inspection
Two provider-specific paths to the actual SQL:
EF Core — ToSqlPreview
string sql = query.ToSqlPreview(); // translated SQL, nothing executed
var plan = query.ExplainProjection(options); // projection plan explanationToSqlPreview uses EF Core's ToQueryString() under the hood and works after dynamic
projections are applied. ExplainProjection returns a ProjectionExplanation: selected
fields, navigation usage, and optimization notes.
Dapper — SQL execution logging
Every Dapper command logs an Information-level entry right before execution under logger
category FlexQuery.NET.Dapper. The entry contains the final SQL formatted for readability,
preceded by a DECLARE block embedding the parameter values — copy-paste-ready:
DECLARE @p0 BIGINT = 42;
SELECT [o].[Id], [o].[Total]
FROM [Orders] AS [o]
WHERE [o].[CustomerId] = @p0The logging helper only reads the SQL and parameters already passed to Dapper — it never mutates or executes anything, and it short-circuits to a no-op when the logger is null or Information level is disabled.
Complete worked example
A timing endpoint for investigating a slow query:
[HttpGet("api/debug/customers")]
public async Task<IActionResult> DebugQuery(
[FromQuery] FlexQueryParameters parameters,
CancellationToken cancellationToken)
{
var collector = new FlexQueryDiagnosticsCollector();
var sw = Stopwatch.StartNew();
var result = await db.Customers
.AsNoTracking()
.FlexQueryAsync(parameters, opt => opt.Listener = collector, cancellationToken);
sw.Stop();
var report = collector.BuildReport(provider: "EF Core", translator: "Sqlite");
return Ok(new
{
result.TotalCount,
elapsedMs = sw.Elapsed.TotalMilliseconds,
diagnostics = report,
});
}SQL formatting
The SQL that reaches reports and Dapper logs is rendered by a shared formatter
(FlexQuery.NET.SqlFormatting) used by both providers — clause-per-line layout and
parameter blocks come from the same component everywhere. It is an implementation detail
rather than a public API; consume the formatted SQL through the listener events, the
collector report, and the logger.