Skip to content
FlexQuery.NET

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:

HookFired whenContains
QueryParsedAsync(QueryParsedEvent e, CancellationToken ct)Parameters parsed into QueryOptionsWhat the client actually asked for
QueryTranslatedAsync(QueryTranslatedEvent e, ct)Provider translated the queryGenerated SQL / LINQ
QueryExecutedAsync(QueryExecutedEvent e, ct)Database command completedExecution timing and outcome
QueryMaterializedAsync(QueryMaterializedEvent e, ct)Results materializedResult-shape details

Attach a listener per request through the execution options:

C#
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.
C#
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

C#
string sql = query.ToSqlPreview();            // translated SQL, nothing executed
var plan = query.ExplainProjection(options);  // projection plan explanation

ToSqlPreview 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:

SQL
DECLARE @p0 BIGINT = 42;

SELECT [o].[Id], [o].[Total]
FROM [Orders] AS [o]
WHERE [o].[CustomerId] = @p0

The 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:

C#
[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.

Common mistakes