Guardrail your database queries in tests & CI. Capture executed SQL, enforce budgets (counts & timings), and fail builds on regressions. Works with ADO.NET, Dapper, and EF Core.
Quick links:
- 👉 Quick Start — Samples (local)
- 👉 EF Core wiring · Dapper wiring · ADO.NET wiring
- 👉 CLI flags · Troubleshooting
Use the disposable scope to export JSON and enforce budgets in your test or smoke app.
using KeelMatrix.QueryWatch.Testing;
using var scope = QueryWatchScope.Start(
maxQueries: 50,
maxAverage: TimeSpan.FromMilliseconds(25),
exportJsonPath: "artifacts/qwatch.report.json",
sampleTop: 200);
// ... run your code that hits the DB ...
The file artifacts/qwatch.report.json
is now ready for the CLI gate.
This repo ships three tiny sample apps (EF Core, ADO.NET, Dapper) that consume local packages you build from source.
- Pack the libraries (run at repo root):
dotnet pack ./src/KeelMatrix.QueryWatch/KeelMatrix.QueryWatch.csproj -c Release --include-symbols --p:SymbolPackageFormat=snupkg --output ./artifacts/packages dotnet pack ./src/KeelMatrix.QueryWatch.EfCore/KeelMatrix.QueryWatch.EfCore.csproj -c Release --include-symbols --p:SymbolPackageFormat=snupkg --output ./artifacts/packages
- Install local packages to samples (pins to
./artifacts/packages
viasamples/NuGet.config
):- Windows (PowerShell):
./samples/init.ps1
- Linux/macOS (bash):
./samples/init.sh
- Windows (PowerShell):
- Run a sample (EF example shown):
dotnet run --project ./samples/EFCore.Sqlite/EFCore.Sqlite.csproj -c Release
- Gate with the CLI:
dotnet run --project ./tools/KeelMatrix.QueryWatch.Cli -- --input ./samples/EFCore.Sqlite/bin/Release/net8.0/artifacts/qwatch.ef.json --max-queries 50
CI uses the same flow and restores using
samples/NuGet.config
so samples build afterdotnet pack
with no tweaks.
using Microsoft.EntityFrameworkCore;
using KeelMatrix.QueryWatch.EfCore;
using KeelMatrix.QueryWatch.Testing;
using var q = QueryWatchScope.Start(exportJsonPath: "artifacts/ef.json", sampleTop: 200);
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite("Data Source=:memory:")
.UseQueryWatch(q.Session) // adds the interceptor
.Options;
Interceptor only records executed commands. Use
QueryWatchOptions
on the session to tune capture (text, parameter shapes, etc.).
using Dapper;
using Microsoft.Data.Sqlite;
using KeelMatrix.QueryWatch.Dapper;
using KeelMatrix.QueryWatch.Testing;
using var q = QueryWatchScope.Start(exportJsonPath: "artifacts/dapper.json", sampleTop: 200);
await using var raw = new SqliteConnection("Data Source=:memory:");
await raw.OpenAsync();
// Wrap the provider connection so Dapper commands are recorded
using var conn = raw.WithQueryWatch(q.Session);
var rows = await conn.QueryAsync("SELECT 1");
The extension returns the ADO wrapper when possible for high‑fidelity recording; otherwise it falls back to a Dapper‑specific wrapper.
using Microsoft.Data.Sqlite;
using KeelMatrix.QueryWatch.Ado;
using KeelMatrix.QueryWatch.Testing;
using var q = QueryWatchScope.Start(exportJsonPath: "artifacts/ado.json", sampleTop: 200);
await using var raw = new SqliteConnection("Data Source=:memory:");
await raw.OpenAsync();
using var conn = new QueryWatchConnection(raw, q.Session);
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT 1";
await cmd.ExecuteNonQueryAsync();
At test time (scope) you can enforce maxQueries
, maxAverage
, maxTotal
. At CI time use the CLI for stronger rules including per‑pattern budgets. Patterns support wildcards (*
, ?
) or regex:
prefix.
Example per‑pattern budgets:
--budget "SELECT * FROM Users*=1"
--budget "regex:^UPDATE Orders SET=3"
If your JSON is top‑N sampled, budgets evaluate only over those events. Increase
sampleTop
in your export to tighten guarantees.
--input <path> Input JSON summary file. (repeatable)
--max-queries N Fail if total query count exceeds N.
--max-average-ms N Fail if average duration exceeds N ms.
--max-total-ms N Fail if total duration exceeds N ms.
--baseline <path> Baseline summary JSON to compare against.
--baseline-allow-percent P Allow +P% regression vs baseline before failing.
--write-baseline Write current aggregated summary to --baseline.
--budget "<pattern>=<max>" Per-pattern query count budget (repeatable). (repeatable)
Pattern supports wildcards (*, ?) or prefix with 'regex:' for raw regex.
--require-full-events Fail if input summaries are top-N sampled.
--help Show this help.
Repeat --input
to aggregate multiple JSONs (e.g., per‑project reports in a mono‑repo). Budgets evaluate on the aggregate.
When run inside GitHub Actions, the CLI writes a Markdown table to the Step Summary automatically.
- “Budget violations:” but no pattern table → you didn’t pass any
--budget
, or your JSON was heavily sampled. Re‑export with highersampleTop
(e.g., 200–500). - Baselines seem too strict → tolerances are percent of baseline. Ensure your baseline is representative; use
--baseline-allow-percent
to allow small drift. - CLI help in README looks stale → run
./build/Update-ReadmeFlags.ps1
(or--print-flags-md
) to refresh the block between markers. - Hot path text capture → disable per‑adapter:
QueryWatchOptions.Disable{Ado|Dapper|EfCore}TextCapture=true
. - Parameter metadata → set
QueryWatchOptions.CaptureParameterShape=true
(emitsevent.meta.parameters
), never values.
MIT