Tracing in RepoDB — See Every Statement, Parameter, and Millisecond

Every call to Query, Insert, Update, or ExecuteQuery methods hide three things you usually care about: the exact SQL that ran, the parameters it ran with, and how long it took. RepoDB exposes all three through a single interface, ITrace, with two hooks: one right before execution and one right after.

Problem: Executions are not transparent by default

When you call connection.Query<Customer>(...), the library builds a statement, sends it, and hands back objects.

Everything in between is invisible by default. When a query is slow, or returns the wrong rows, or a parameter carries an unexpected value, there is nothing to inspect without attaching a profiler or a full APM agent.

Tracing closes that gap directly inside the library. It gives you the possibilites to intercept, analyze and audit every execution sent.

The trace hook, built in

ITrace has two methods:

  • BeforeExecution(CancellableTraceLog log) — runs right before the command reaches the database.
  • AfterExecution<TResult>(ResultTraceLog<TResult> log) — runs right after it comes back.

Both are called on every traced operation, keyed by operation name so a traced Insert never gets confused with a traced Query.

What you can see

Each hook receives a log object with everything you need:

  • Statement — the exact SQL RepoDB composed.
  • Parameter — the parameter values bound to that statement.
  • SessionId and Key — identifiers for correlating a before/after pair, or one call across a distributed trace.
  • ExecutionTime — the elapsed time, available on AfterExecution via ResultTraceLog.
  • Result — the value the operation is about to return, also on AfterExecution.

No estimation, no separate logging layer. The exact statement and parameters, straight from the code that composed them.

Writing a trace class

public class NorthwindTrace : ITrace
{
    public void BeforeExecution(CancellableTraceLog log) { }

    public void AfterExecution<TResult>(ResultTraceLog<TResult> log) =>
        logger.Info($"{log.Statement} took {log.ExecutionTime.TotalMilliseconds}ms");
}

connection.Insert<Customer>(customer, trace: new NorthwindTrace());

That single line in AfterExecution already answers the question every slow endpoint eventually raises: which statement, and how long.

Injecting it into a repository

Passing trace: at every call site works, but it does not scale past a handful of calls. Pass the trace instance into a repository’s constructor instead, and every operation made through it is traced automatically.

public class CustomerRepository : BaseRepository<Customer, SqlConnection>
{
    public CustomerRepository(IOptions<AppSettings> settings)
        : base(settings.Value.ConnectionString, new NorthwindTrace()) { }
}

For dependency injection, extend ITrace with your own interface, register the implementation as a singleton, and let the container supply it.

public interface INorthwindTrace : ITrace { }

public class NorthwindTrace : INorthwindTrace { ... }

services.AddSingleton<INorthwindTrace, NorthwindTrace>();

public class NorthwindRepository : DbRepository<SqlConnection>
{
    public NorthwindRepository(IOptions<AppSettings> settings, INorthwindTrace trace)
        : base(settings.Value.ConnectionString, trace) { }
}

One trace instance, shared across the application, is the right shape. Creating a new one per call defeats any correlation you were hoping to get across calls.

Cancelling a suspicious execution

BeforeExecution runs before the statement reaches the server, which makes it a checkpoint, not just a log line. Call log.Cancel(true) and the operation stops cold.

public void BeforeExecution(CancellableTraceLog log)
{
    var blocked = new[] { "DROP", "ALTER", "TRUNCATE" };
    if (blocked.Any(word => log.Statement.ToUpper().Contains(word)))
    {
        log.Cancel(true);
    }
}

Cancel(true) also throws an exception back to the caller, so a cancelled operation never gets mistaken for one that quietly succeeded.

A real-world example: telemetry built on tracing

RepoDB’s own default telemetry package, RepoDb.Telemetry.Default, is not a separate instrumentation layer bolted onto the library. It is an ITrace implementation. TelemetryTrace buffers a telemetry item in BeforeExecution/AfterExecution and flushes it on a timer — the exact same two hooks described above, doing production-grade observability instead of a Console.WriteLine. If you have ever wondered how RepoDB’s insights dashboard knows what ran and how long it took, this is how.

Good candidates for this pattern

  • Logging every statement and its elapsed time during development or in a staging environment.
  • Flagging statements that run longer than an expected threshold.
  • Auditing who changed what, by capturing the statement and parameters on every write.
  • Guarding against a dynamically built statement that does not match what was expected.
  • Feeding a metrics or telemetry pipeline, the way TelemetryTrace does internally.

Things to keep in mind

  • Keep BeforeExecution and AfterExecution fast. They run inline with every traced call, so slow logging becomes slow queries.
  • Use one shared trace instance, not one per call, so correlation and any internal buffering actually work.
  • Parameters can carry sensitive data. Redact or omit fields you would not want sitting in a log file.

Learn more

See ITrace for the full interface, TraceLog, CancellableTraceLog, and ResultTraceLog for what each hook receives, and Telemetry for the built-in trace implementation described above.

Please support us

Star our Github repository | Follow me at X / Twitter | Connect with us at our official Teams Channel
Apache License 2.0 — Copyright © 2026 by Michael Camara Pendon / Create a Request


~ Conceptualized and written by me. Reviewed and checked by AI. Refined and gatekept by me. ~