Skip to content

Commit f0570d8

Browse files
committed
chore: add CLI project, pin System.Text.Json 8.0.5, update CI gate and docs
1 parent 163ce75 commit f0570d8

23 files changed

+810
-237
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ jobs:
2727
run: dotnet build --configuration Release --no-restore
2828
- name: Test
2929
run: dotnet test --configuration Release --no-build --collect:"XPlat Code Coverage" --results-directory ./artifacts/TestResults
30+
- name: QueryWatch gate (optional - JSON)
31+
shell: pwsh
32+
run: |
33+
if (Test-Path "artifacts/qwatch.report.json") {
34+
dotnet run --project tools/KeelMatrix.QueryWatch.Cli -- --input artifacts/qwatch.report.json --max-queries 50
35+
} else {
36+
Write-Host "No QueryWatch JSON found; skipping QueryWatch gate."
37+
}
3038
- name: Pack
3139
run: dotnet pack --configuration Release --no-build --include-symbols --p:SymbolPackageFormat=snupkg --output ./artifacts/packages
3240
- name: Upload packages
@@ -57,8 +65,3 @@ jobs:
5765
# if: always()
5866
# run: |
5967
# echo "TODO: Scan changed fixture files for PII patterns"
60-
#
61-
# - name: Query performance budget
62-
# if: always()
63-
# run: |
64-
# echo "TODO: Check test output for query counts / durations and enforce a threshold"

Directory.Packages.props

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<PackageVersion Include="xunit" Version="2.7.1" />
1212
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
1313
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
14-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
14+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
15+
16+
<!-- JSON serializer used by QueryWatch.Reporting -->
17+
<!-- Pin a patched System.Text.Json so NuGet never resolves 8.0.0 -->
18+
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
1519
</ItemGroup>
1620
</Project>

KeelMatrix.QueryWatch.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeelMatrix.QueryWatch", "sr
66
EndProject
77
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeelMatrix.QueryWatch.Tests", "tests\KeelMatrix.QueryWatch.Tests\KeelMatrix.QueryWatch.Tests.csproj", "{22222222-2222-2222-2222-222222222222}"
88
EndProject
9+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeelMatrix.QueryWatch.Cli", "tools\KeelMatrix.QueryWatch.Cli\KeelMatrix.QueryWatch.Cli.csproj", "{33333333-3333-3333-3333-333333333333}"
10+
EndProject
911
Global
1012
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1113
Debug|Any CPU = Debug|Any CPU
@@ -20,6 +22,10 @@ Global
2022
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
2123
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
2224
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU
25+
{33333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26+
{33333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU
27+
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU
28+
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU
2329
EndGlobalSection
2430
GlobalSection(SolutionProperties) = preSolution
2531
HideSolutionNode = FALSE

README.md

Lines changed: 30 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,57 @@
11
# KeelMatrix.QueryWatch
22

3-
[![Build](https://github.com/OWNER/REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/OWNER/REPO/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/KeelMatrix.QueryWatch.svg)](https://www.nuget.org/packages/KeelMatrix.QueryWatch/)
4-
5-
**For developers building tools and libraries, `KeelMatrix.QueryWatch` delivers a working NuGet package in minutes without the usual boilerplate.**
6-
7-
This repository is a template that bundles together a library project, an xUnit test project, a sample console application, common build configuration, GitHub workflows, licensing and telemetry stubs, and a full suite of documentation. It allows you to start publishing high‑quality NuGet packages with minimal effort.
3+
> Catch N+1 queries and slow SQL in tests. Fail builds when query budgets are exceeded.
84
9-
## Getting started
10-
11-
1. Clone or download this template.
12-
2. Rename the solution and default project names when prompted to match your package name.
13-
3. Open the solution in Visual Studio 2022 or run `dotnet build` from the command line.
5+
[![Build](https://github.com/OWNER/REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/OWNER/REPO/actions/workflows/ci.yml) [![NuGet](https://img.shields.io/nuget/v/KeelMatrix.QueryWatch.svg)](https://www.nuget.org/packages/KeelMatrix.QueryWatch/)
146

15-
### Install from NuGet
7+
## Install
168

179
```bash
1810
dotnet add package KeelMatrix.QueryWatch
1911
```
2012

21-
### Quickstart
13+
## 5‑minute success (with JSON for CI)
2214

23-
Here's how to use the sample API exposed by the template:
15+
**Per‑test scope → export JSON:**
2416

2517
```csharp
26-
using KeelMatrix.QueryWatch;
18+
using KeelMatrix.QueryWatch.Testing;
2719

28-
var hello = new Hello();
29-
Console.WriteLine(hello.Greet("World"));
30-
```
31-
32-
You can also explore the sample console application by running:
20+
// JSON is written even if assertions fail (helps CI).
21+
using var q = QueryWatchScope.Start(
22+
maxQueries: 5,
23+
maxAverage: TimeSpan.FromMilliseconds(50),
24+
exportJsonPath: "artifacts/qwatch.report.json");
3325

34-
```bash
35-
cd samples/KeelMatrix.QueryWatch.Sample
36-
dotnet run
26+
// wire EF Core or ADO to q.Session, run your code...
3727
```
3828

39-
## Target frameworks
40-
41-
This package targets the following frameworks:
42-
43-
* `net8.0` – for modern .NET applications.
44-
* `netstandard2.0` – for broad compatibility with .NET Framework and .NET Core.
29+
**Gate in CI (already wired in ci.yml):**
4530

46-
## Versioning and releases
47-
48-
This project follows [Semantic Versioning](https://semver.org/). Breaking changes or removal of a target framework require a new major version. New features that do not break existing behavior increment the minor version. Patch versions are used for bug fixes and small improvements. Pre‑release packages use suffixes such as `-alpha`, `-beta`, or `-rc`.
49-
50-
Release notes are maintained in [`CHANGELOG.md`](CHANGELOG.md). To create a new release:
51-
52-
1. Update the version in the library’s `.csproj` file and add an entry in the changelog.
53-
2. Commit your changes and tag the commit (for example `git tag v1.0.0`).
54-
3. Push the tag. GitHub Actions will build, sign, and publish the package to nuget.org (assuming you have configured `NUGET_API_KEY` in your repository secrets).
55-
56-
## Documentation
57-
58-
* [`LICENSE`](LICENSE) – The license this project is released under (MIT by default).
59-
* [`SECURITY.md`](SECURITY.md) – How to report security issues.
60-
* [`CONTRIBUTING.md`](CONTRIBUTING.md) – Guidelines for contributors.
61-
* [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) – The code of conduct for this project.
62-
* [`PRIVACY.md`](PRIVACY.md) – Information about telemetry and how to disable it.
63-
64-
## FAQ
65-
66-
### How do I build and test the package locally?
67-
68-
Run the following commands from the repository root:
69-
70-
```bash
71-
dotnet restore
72-
dotnet build --configuration Release
73-
dotnet test --configuration Release --no-build
74-
dotnet pack --configuration Release --no-build --output ./artifacts/packages
31+
```pwsh
32+
dotnet run --project tools/KeelMatrix.QueryWatch.Cli -- --input artifacts/qwatch.report.json --max-queries 50
7533
```
7634

77-
The resulting `.nupkg` and `.snupkg` files can be found in `./artifacts/packages`.
78-
79-
### How do I consume the package without publishing it to NuGet?
80-
81-
Update your `NuGet.config` to add a local feed pointing at the `artifacts/packages` folder. For example:
82-
83-
```xml
84-
<?xml version="1.0" encoding="utf-8"?>
85-
<configuration>
86-
<packageSources>
87-
<add key="local" value="./artifacts/packages" />
88-
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
89-
</packageSources>
90-
</configuration>
91-
```
92-
93-
Then run `dotnet restore` in your consuming project.
94-
95-
### How do I add paid features or telemetry?
96-
97-
The template includes stub interfaces `ILicenseValidator` and `ITelemetryClient` with no‑op implementations. Replace these with your own implementations and wire them into your API as needed. See comments in the source code for guidance.
98-
99-
---
100-
101-
_This README is intentionally generic. Replace the sample code and descriptive text with information relevant to your package._
102-
103-
## Why this template (promise line)
104-
105-
For developers shipping .NET libraries quickly: this template gets you from **idea → publishable NuGet** in minutes, with tests, CI, SourceLink, symbols, docs, and repo hygiene baked in.
106-
107-
## Supported TFMs
108-
109-
- `net8.0`
110-
- `netstandard2.0`
111-
112-
## Release & versioning policy
113-
114-
- **SemVer**: PATCH=fixed bugs, MINOR=new features (no breaking changes), MAJOR=breaking changes or dropping a TFM.
115-
- Use pre-releases: `-alpha`, `-beta`, `-rc` as needed.
116-
- Tag releases as `vX.Y.Z`; CI picks up tags to publish.
117-
- Maintain `CHANGELOG.md` for notable changes.
118-
119-
## NuGet ID & branding
120-
121-
- Choose a consistent **package ID prefix** (e.g., `KeelMatrix.*`).
122-
- When ready, **reserve your prefix** on nuget.org (TODO: add link).
123-
- Set **Authors**, **RepositoryUrl**, **PackageProjectUrl**, **icon** in the `.csproj`.
124-
125-
## CI artifacts
126-
127-
CI uploads built packages to `./artifacts/packages` as workflow artifacts you can download.
35+
**EF Core:** see `src/KeelMatrix.QueryWatch/README.md` for full example.
12836

129-
## Licensing & monetization stubs
37+
## Why QueryWatch?
13038

131-
This template includes `ILicenseValidator` + `NoopLicenseValidator` and doc notes to wire a MoR (Paddle/Lemon Squeezy). Mark paid features in code and validate via your chosen MoR before enabling. Include an **offline grace** policy (e.g., 7–30 days).
39+
- **Prevents N+1 and slow queries** before they reach production.
40+
- **Lightweight**: plug into EF Core or wrap ADO/Dapper connection.
41+
- **Redaction hooks**: mask PII or noisy literals.
42+
- **CI‑friendly**: export JSON and use the CLI gate to fail PRs.
13243

133-
## Telemetry (optional, off by default)
44+
## JSON schema
13445

135-
Implements `ITelemetryClient` with a no‑op default. If you later enable telemetry, publish a clear privacy note and allow opt‑out via `TOOLNAME_NO_TELEMETRY=1`.
46+
Stable, compact summary emitted by `KeelMatrix.QueryWatch.Reporting.QueryWatchJson.ExportToFile(report, path)` with fields:
47+
`schema, startedAt, stoppedAt, totalQueries, totalDurationMs, averageDurationMs, events[]`.
13648

137-
## Release checklist
49+
## CLI
13850

139-
- [ ] Tests pass on Release configuration
140-
- [ ] `dotnet pack` produces one `.nupkg` and one `.snupkg`
141-
- [ ] Version bumped in the `.csproj`
142-
- [ ] Tag created `vX.Y.Z`
143-
- [ ] CI artifacts verified; (optional) nuget.org push succeeded
51+
- `--input` path to JSON (default `artifacts/qwatch.report.json`)
52+
- `--max-queries`, `--max-average-ms`, `--max-total-ms`
53+
- `--baseline <file>` and `--write-baseline` to store today’s good results
14454

55+
## License
14556

146-
> **Note:** Consider adopting [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) later for repo-driven versioning (optional; not bundled in the template).
57+
MIT. See `LICENSE`. See `PRIVACY.md` for telemetry stance (off by default).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#nullable enable
2+
using System.Data;
3+
using System.Data.Common;
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace KeelMatrix.QueryWatch.Ado
8+
{
9+
/// <summary>
10+
/// Delegating <see cref="DbCommand"/> that measures execution and records into a session.
11+
/// </summary>
12+
public sealed class QueryWatchCommand : DbCommand
13+
{
14+
private readonly DbCommand _inner;
15+
private readonly QueryWatchSession _session;
16+
private readonly DbConnection? _connection; // wrapper connection
17+
18+
public QueryWatchCommand(DbCommand inner, QueryWatchSession session, DbConnection? wrapperConnection = null)
19+
{
20+
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
21+
_session = session ?? throw new ArgumentNullException(nameof(session));
22+
_connection = wrapperConnection;
23+
}
24+
25+
[AllowNull]
26+
public override string CommandText
27+
{
28+
get => _inner.CommandText;
29+
set => _inner.CommandText = value;
30+
}
31+
32+
public override int CommandTimeout
33+
{
34+
get => _inner.CommandTimeout;
35+
set => _inner.CommandTimeout = value;
36+
}
37+
38+
public override CommandType CommandType
39+
{
40+
get => _inner.CommandType;
41+
set => _inner.CommandType = value;
42+
}
43+
44+
protected override DbConnection? DbConnection
45+
{
46+
get => _connection ?? _inner.Connection;
47+
set
48+
{
49+
if (value is null) {
50+
_inner.Connection = null;
51+
}
52+
else if (value is QueryWatchConnection qwc) {
53+
_inner.Connection = qwc.Inner;
54+
}
55+
else {
56+
_inner.Connection = value;
57+
}
58+
}
59+
}
60+
61+
protected override DbParameterCollection DbParameterCollection => _inner.Parameters;
62+
63+
protected override DbTransaction? DbTransaction
64+
{
65+
get => _inner.Transaction;
66+
set => _inner.Transaction = value;
67+
}
68+
69+
public override bool DesignTimeVisible
70+
{
71+
get => _inner.DesignTimeVisible;
72+
set => _inner.DesignTimeVisible = value;
73+
}
74+
75+
public override UpdateRowSource UpdatedRowSource
76+
{
77+
get => _inner.UpdatedRowSource;
78+
set => _inner.UpdatedRowSource = value;
79+
}
80+
81+
public override void Cancel() => _inner.Cancel();
82+
public override void Prepare() => _inner.Prepare();
83+
84+
protected override DbParameter CreateDbParameter() => _inner.CreateParameter();
85+
86+
private void Record(TimeSpan elapsed) => _session.Record(_inner.CommandText ?? string.Empty, elapsed);
87+
88+
public override int ExecuteNonQuery()
89+
{
90+
var sw = Stopwatch.StartNew();
91+
try { return _inner.ExecuteNonQuery(); }
92+
finally { sw.Stop(); Record(sw.Elapsed); }
93+
}
94+
95+
public override object? ExecuteScalar()
96+
{
97+
var sw = Stopwatch.StartNew();
98+
try { return _inner.ExecuteScalar(); }
99+
finally { sw.Stop(); Record(sw.Elapsed); }
100+
}
101+
102+
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
103+
{
104+
var sw = Stopwatch.StartNew();
105+
try { return _inner.ExecuteReader(behavior); }
106+
finally { sw.Stop(); Record(sw.Elapsed); }
107+
}
108+
109+
public override async Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken)
110+
{
111+
var sw = Stopwatch.StartNew();
112+
try { return await _inner.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); }
113+
finally { sw.Stop(); Record(sw.Elapsed); }
114+
}
115+
116+
public override async Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken)
117+
{
118+
var sw = Stopwatch.StartNew();
119+
try { return await _inner.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); }
120+
finally { sw.Stop(); Record(sw.Elapsed); }
121+
}
122+
123+
protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
124+
{
125+
var sw = Stopwatch.StartNew();
126+
try { return await _inner.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false); }
127+
finally { sw.Stop(); Record(sw.Elapsed); }
128+
}
129+
130+
protected override void Dispose(bool disposing)
131+
{
132+
if (disposing) _inner.Dispose();
133+
base.Dispose(disposing);
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)