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.SessionIdandKey— identifiers for correlating a before/after pair, or one call across a distributed trace.ExecutionTime— the elapsed time, available onAfterExecutionviaResultTraceLog.Result— the value the operation is about to return, also onAfterExecution.
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
TelemetryTracedoes internally.
Things to keep in mind
- Keep
BeforeExecutionandAfterExecutionfast. 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. ~