Skip to content

Commit 56b67d1

Browse files
committed
Add Dapper.Sqlite sample (async + tx), introduce ADO QueryWatchTransaction for parity, update docs on redactor ordering & Dapper budgets, and add central Dapper package version
1 parent f83d93c commit 56b67d1

File tree

13 files changed

+206
-3
lines changed

13 files changed

+206
-3
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
1919
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
2020
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
21-
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
21+
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageVersion Include="Dapper" Version="2.1.35" />
2222
</ItemGroup>
2323
</Project>

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,30 @@ When run inside GitHub Actions, the CLI writes a Markdown table to the **Step Su
7272
### Note on per‑pattern budgets
7373

7474
Budgets match against the `events` captured in the JSON file(s). These are the top‑N slowest events by duration to keep files small. If you want strict coverage, export with a higher `sampleTop` in `QueryWatchJson.ExportToFile`, or pass a larger `sampleTop` to `QueryWatchScope.Start(...)`.
75+
76+
## Redactor ordering tips
77+
78+
If you use multiple redactors, **order matters**. A safe, effective default is:
79+
80+
1. **Whitespace normalizer** – make SQL text stable across environments/providers.
81+
2. **High‑entropy token masks** – long hex tokens, JWTs, API keys.
82+
3. **PII masks** – emails, phone numbers, IPs.
83+
4. **Custom rules** – your app–specific patterns (use `AddRegexRedactor(...)`).
84+
85+
> Put *broad* rules (like whitespace) first, and *specific* rules (like PII) after. This lowers the chance one rule prevents another from matching.
86+
87+
## Typical budgets for Dapper‑heavy solutions
88+
89+
Dapper often issues *fewer, more targeted* commands than ORMs. Reasonable starting points (tune per project):
90+
91+
- **End‑to‑end web test:** `--max-queries 40`, `--max-average-ms 50`, `--max-total-ms 1500`.
92+
- **Repository‑level test:** `--max-queries 10`, `--max-average-ms 25`, `--max-total-ms 250`.
93+
- **Per‑pattern budgets:** cap hot spots explicitly, e.g.:
94+
95+
```
96+
--budget "SELECT * FROM Users*=5" --budget "regex:^UPDATE Orders SET=3"
97+
```
98+
99+
- Increase `sampleTop` in code (`QueryWatchScope.Start(..., sampleTop: 200)`) if you rely on many per‑pattern budgets.
100+
101+
Treat these as **guardrails**: keep design flexible but catch accidental N+1s or slow queries early.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<PropertyGroup>
10+
<NoWarn>$(NoWarn);1591</NoWarn>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<!-- Version comes from Directory.Packages.props -->
15+
<PackageReference Include="KeelMatrix.QueryWatch" />
16+
<PackageReference Include="Dapper" />
17+
<PackageReference Include="Microsoft.Data.Sqlite" />
18+
</ItemGroup>
19+
</Project>

samples/Dapper.Sqlite/Program.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Dapper;
2+
using DapperSample;
3+
using KeelMatrix.QueryWatch.Dapper;
4+
using KeelMatrix.QueryWatch.Testing;
5+
using Microsoft.Data.Sqlite;
6+
7+
// Dapper + SQLite sample (async + transactions)
8+
var artifacts = Path.Combine(AppContext.BaseDirectory, "artifacts");
9+
Directory.CreateDirectory(artifacts);
10+
var outJson = Path.Combine(artifacts, "qwatch.dapper.json");
11+
12+
using var q = QueryWatchScope.Start(
13+
maxQueries: 50,
14+
maxAverage: TimeSpan.FromMilliseconds(200),
15+
exportJsonPath: outJson,
16+
sampleTop: 50);
17+
18+
// Create an in-memory DB
19+
using var raw = new SqliteConnection("Data Source=:memory:");
20+
await raw.OpenAsync();
21+
22+
// Wrap with QueryWatch (returns QueryWatchConnection under the hood for SQLite)
23+
using var conn = raw.WithQueryWatch(q.Session);
24+
25+
// Create a table
26+
await conn.ExecuteAsync(Redaction.Apply("/* contact: admin@example.com */ CREATE TABLE Users(Id INTEGER PRIMARY KEY, Name TEXT NOT NULL);"));
27+
28+
// Insert in a transaction (exercise Transaction wrapper + async APIs)
29+
using (var tx = conn.BeginTransaction()) {
30+
for (int i = 0; i < 3; i++) {
31+
var email = $"user{i}@example.com"; // will be redacted in CommandText
32+
await conn.ExecuteAsync(
33+
Redaction.Apply($"/* email: {email} */ INSERT INTO Users(Name) VALUES (@name);"),
34+
new { name = Redaction.Param("User_" + i) },
35+
transaction: tx);
36+
}
37+
tx.Commit();
38+
}
39+
40+
// Query back (async)
41+
var total = await conn.ExecuteScalarAsync<int>(Redaction.Apply("SELECT COUNT(*) FROM Users WHERE Name LIKE 'User_%';"));
42+
Console.WriteLine($"Users in DB: {total}");
43+
44+
Console.WriteLine($"QueryWatch JSON written to: {outJson}");
45+
Console.WriteLine("Try the CLI gate:");
46+
Console.WriteLine($" dotnet run --project ../../tools/KeelMatrix.QueryWatch.Cli -- --input '{outJson}' --max-queries 50");
47+

samples/Dapper.Sqlite/Redaction.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace DapperSample {
4+
/// <summary>
5+
/// Simple redaction helper for the sample (emails + long hex tokens)
6+
/// </summary>
7+
internal static class Redaction {
8+
// Very small demo rules; expand as needed
9+
private static readonly Regex Email = new(@"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}", RegexOptions.Compiled);
10+
private static readonly Regex HexToken = new(@"\b[0-9a-f]{32,}\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
11+
12+
public static string Apply(string input) =>
13+
HexToken.Replace(Email.Replace(input, "***"), "***");
14+
15+
public static object Param(object? v) => v is string s ? Apply(s) : v ?? DBNull.Value;
16+
}
17+
}

samples/QueryWatch.Samples.sln

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
Microsoft Visual Studio Solution File, Format Version 12.00
23
# Visual Studio Version 17
34
VisualStudioVersion = 17.0.31903.59
@@ -6,20 +7,54 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCore.Sqlite", "EFCore.Sql
67
EndProject
78
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ado.Sqlite", "Ado.Sqlite\Ado.Sqlite.csproj", "{17F5B486-A115-428F-92D3-5F57AFA585AA}"
89
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapper.Sqlite", "Dapper.Sqlite\Dapper.Sqlite.csproj", "{90202549-95A3-48CB-AEBF-58C370B16150}"
11+
EndProject
912
Global
1013
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1114
Debug|Any CPU = Debug|Any CPU
15+
Debug|x64 = Debug|x64
16+
Debug|x86 = Debug|x86
1217
Release|Any CPU = Release|Any CPU
18+
Release|x64 = Release|x64
19+
Release|x86 = Release|x86
1320
EndGlobalSection
1421
GlobalSection(ProjectConfigurationPlatforms) = postSolution
1522
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1623
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|x64.ActiveCfg = Debug|Any CPU
25+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|x64.Build.0 = Debug|Any CPU
26+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|x86.ActiveCfg = Debug|Any CPU
27+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Debug|x86.Build.0 = Debug|Any CPU
1728
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|Any CPU.ActiveCfg = Release|Any CPU
1829
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|Any CPU.Build.0 = Release|Any CPU
30+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|x64.ActiveCfg = Release|Any CPU
31+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|x64.Build.0 = Release|Any CPU
32+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|x86.ActiveCfg = Release|Any CPU
33+
{32D2847B-FBCA-46A3-AC89-95BBE0081607}.Release|x86.Build.0 = Release|Any CPU
1934
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2035
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|x64.ActiveCfg = Debug|Any CPU
37+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|x64.Build.0 = Debug|Any CPU
38+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|x86.ActiveCfg = Debug|Any CPU
39+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Debug|x86.Build.0 = Debug|Any CPU
2140
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
2241
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|x64.ActiveCfg = Release|Any CPU
43+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|x64.Build.0 = Release|Any CPU
44+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|x86.ActiveCfg = Release|Any CPU
45+
{17F5B486-A115-428F-92D3-5F57AFA585AA}.Release|x86.Build.0 = Release|Any CPU
46+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|Any CPU.Build.0 = Debug|Any CPU
48+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|x64.ActiveCfg = Debug|Any CPU
49+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|x64.Build.0 = Debug|Any CPU
50+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|x86.ActiveCfg = Debug|Any CPU
51+
{90202549-95A3-48CB-AEBF-58C370B16150}.Debug|x86.Build.0 = Debug|Any CPU
52+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|Any CPU.Build.0 = Release|Any CPU
54+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|x64.ActiveCfg = Release|Any CPU
55+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|x64.Build.0 = Release|Any CPU
56+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|x86.ActiveCfg = Release|Any CPU
57+
{90202549-95A3-48CB-AEBF-58C370B16150}.Release|x86.Build.0 = Release|Any CPU
2358
EndGlobalSection
2459
GlobalSection(SolutionProperties) = preSolution
2560
HideSolutionNode = FALSE

samples/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ quickly in different situations (EF Core + SQLite, and raw ADO).
1010
## Layout
1111
- `EFCore.Sqlite/` – EF Core (SQLite provider) showing interceptor wiring and basic budgets.
1212
- `Ado.Sqlite/` – plain ADO.NET over `Microsoft.Data.Sqlite` wrapped by `QueryWatchConnection`.
13+
- `Dapper.Sqlite/` – tiny Dapper example over SQLite showing **async** and **transaction** usage with QueryWatch.
1314
- `cli-examples.ps1` / `cli-examples.sh` – example commands for running the QueryWatch CLI gate.
1415
- `NuGet.config` – forces the `KeelMatrix.QueryWatch*` packages to come from your local `../artifacts/packages`.
1516
- `.gitignore` – ignores local build outputs and DB files for samples only.
@@ -44,3 +45,9 @@ quickly in different situations (EF Core + SQLite, and raw ADO).
4445
- These samples **compile only after** you add the `KeelMatrix.QueryWatch` and (for EF) `KeelMatrix.QueryWatch.EfCore` packages (Step 2).
4546
- If you get restore errors, confirm that `../artifacts/packages` exists and contains your `*.nupkg` files.
4647
- The EF Core sample uses a file-based SQLite DB under `./EFCore.Sqlite/app.db`. You can delete it safely.
48+
49+
### Dapper sample
50+
Run the Dapper sample:
51+
```bash
52+
dotnet run --project ./Dapper.Sqlite/Dapper.Sqlite.csproj
53+
```

samples/init.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ dotnet --info | Out-Null
66
dotnet add ./EFCore.Sqlite/EFCore.Sqlite.csproj package KeelMatrix.QueryWatch
77
dotnet add ./EFCore.Sqlite/EFCore.Sqlite.csproj package KeelMatrix.QueryWatch.EfCore
88
dotnet add ./Ado.Sqlite/Ado.Sqlite.csproj package KeelMatrix.QueryWatch
9+
dotnet add ./Dapper.Sqlite/Dapper.Sqlite.csproj package KeelMatrix.QueryWatch
910

1011
Write-Host "Restore completed. You can now run the samples." -ForegroundColor Green
12+

samples/init.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ dotnet --info >/dev/null
66
dotnet add ./EFCore.Sqlite/EFCore.Sqlite.csproj package KeelMatrix.QueryWatch
77
dotnet add ./EFCore.Sqlite/EFCore.Sqlite.csproj package KeelMatrix.QueryWatch.EfCore
88
dotnet add ./Ado.Sqlite/Ado.Sqlite.csproj package KeelMatrix.QueryWatch
9+
dotnet add ./Dapper.Sqlite/Dapper.Sqlite.csproj package KeelMatrix.QueryWatch
910

1011
echo "Restore completed. You can now run the samples."
12+

src/KeelMatrix.QueryWatch/Ado/QueryWatchCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ protected override DbConnection DbConnection {
5757

5858
protected override DbTransaction? DbTransaction {
5959
get => _inner.Transaction;
60-
set => _inner.Transaction = value;
60+
set {
61+
if (value is QueryWatchTransaction wrap) _inner.Transaction = wrap.Inner;
62+
else _inner.Transaction = value;
63+
}
6164
}
6265

6366
public override bool DesignTimeVisible {
@@ -160,3 +163,4 @@ protected override void Dispose(bool disposing) {
160163
}
161164
}
162165
}
166+

0 commit comments

Comments
 (0)