SqlBound is a .NET library providing SQLx-equivalent functionality for C#: SQL queries verified at compile time against a real database schema, reflection-free row materialization via a Roslyn incremental source generator, SQL-file migrations, and clean coexistence with Dapper in the same project.
Status: v1.0.0-rc.1 — release candidate under review. Phases 1–5 (Bedrock, Codegen, Verification,
Providers, Migrations & CLI) are complete, and Phase 6 (Ship — API freeze and the 1.0 release) is in
progress; the stable 1.0.0 follows once the candidate is validated. See Roadmap and the
changelog.
Dapper maps hand-written, runtime-composed SQL with reflection. SqlBound targets the other half of the problem: SQL that is static and known at build time. For that SQL, it generates straight-line ADO.NET reader code ahead of time (no reflection, no IL emit — Native AOT and trimming compatible) and verifies the query's shape against the database before it ever runs, turning a class of runtime SQL errors into compiler diagnostics.
The two libraries are designed to coexist on the same DbConnection/DbTransaction with zero
conflict — SqlBound never owns a connection and never defines Query*/Execute* extension
methods that would collide with Dapper's. See
docs/dapper-coexistence.md for a side-by-side guide.
using System.Data.Common;
using SqlBound;
public static partial class ItemQueries
{
[SqlQuery("SELECT id AS Id, name AS Name, price AS Price FROM items WHERE category = @category ORDER BY id")]
public static partial Task<IReadOnlyList<Item>> GetByCategoryAsync(
DbConnection connection, string category, CancellationToken cancellationToken = default);
[SqlExecute("DELETE FROM items WHERE category = @category")]
public static partial Task<int> DeleteByCategoryAsync(
DbConnection connection, string category, CancellationToken cancellationToken = default);
}
public sealed record Item(int Id, string Name, decimal? Price);The generator emits the method bodies. Result shapes are inferred from the return type:
Task<T> (single row, throws if not exactly one), Task<T?> (single row or none),
Task<IReadOnlyList<T>>, IAsyncEnumerable<T> (streaming), scalar types (Task<int>,
Task<string>, …), and Task/Task<int> for [SqlExecute] statements.
For SQL that isn't known until runtime — dynamic filters, user-composed queries — SqlSession
gives Dapper-style access (RunAsync, distinct verbs so the two APIs never collide) without a
generator in the loop.
An opt-in, two-stage pipeline (mirroring SQLx's own offline .sqlx mode):
dotnet sqlbound prepare— an explicit CLI step that connects to a real database (viaSQLBOUND_DATABASE_URL), describes every[SqlQuery]/[SqlExecute]statement's columns and parameters, and commits the result as JSON snapshots under.sqlbound/. This is the only place a live database connection is opened; it never happens during a normal build.- The analyzer reads only those committed snapshots — offline, on every build and in the
IDE — and reports
SQLB101–SQLB111for missing/stale snapshots and column, type, or parameter mismatches against the method's declared C# signature.
Projects that never run prepare never see a verification diagnostic (opt-in by snapshot
presence). See docs/verification.md, docs/diagnostics.md,
and docs/introspection.md for the full workflow, the diagnostic catalog,
and what each provider can and cannot describe.
Schema changes are ordered SQL-file migrations — paired {version}_{name}.up.sql / .down.sql
files with a timestamp version (the down file optional per migration) — tracked in a
_sqlbound_migrations ledger that checksums each applied script. The sqlbound tool drives the
whole lifecycle:
dotnet sqlbound migrate add "create items" # scaffold a timestamped up/down pair
dotnet sqlbound migrate run # apply every pending migration, in order
dotnet sqlbound migrate status # applied / pending / drifted / missing
dotnet sqlbound migrate revert # roll back the most recent migration
dotnet sqlbound database create # create or drop the target databasemigrate run applies each migration in its own transaction (where the provider supports it),
refusing to proceed on checksum drift or an out-of-order migration. See
docs/migrations.md for the file format, the ledger, and per-provider
behaviour. Design decisions are in ADR 0006 and
ADR 0007.
SQL Server is the pilot; SQLite, PostgreSQL, and MySQL followed. The provider is selected from the
SQLBOUND_DATABASE_URL scheme (sqlserver://, sqlite://, postgresql://, mysql://).
| Provider | Verification (prepare) |
Migrations & database |
Notes |
|---|---|---|---|
| SQL Server | Full (columns + parameters) | Yes, transactional | Pilot provider |
| PostgreSQL | Full (columns + parameters) | Yes, transactional | |
| SQLite | Columns only; computed columns rejected (ADR 0005) | Yes, transactional | The Data Source file is the database |
| MySQL | Columns only; no parameter typing | Yes, not transactional (ADR 0007) | DDL auto-commits |
| Package | Purpose |
|---|---|
SqlBound |
Runtime core — attributes, SqlSession, dependency-free. |
SqlBound.Generators |
The incremental source generator and the verification analyzer. Packed as an analyzer, never a runtime dependency. |
SqlBound.Introspection |
Provider-neutral introspection contracts (IQueryDescriber). |
SqlBound.Migrations |
Provider-neutral migration model, the IMigrationLedger history contract, and IDatabaseAdmin. |
SqlBound.SqlServer / .Sqlite / .Npgsql / .MySql |
Per-provider introspection, type mapping, migration ledger, and database administration. |
SqlBound.Cli |
The sqlbound dotnet tool — prepare, migrate add/run/revert/status, and database create/drop. |
Architectural decisions are recorded as ADRs in docs/adr/:
- 0001 — split verification between the CLI/MSBuild task (I/O) and the analyzer (offline)
- 0002 — the generator emits from the declared signature alone; it never reads snapshots
- 0003 — a project with no
.sqlbound/snapshots gets no verification diagnostics - 0004 —
preparestays a CLI step; an MSBuild task is deferred - 0005 — SQLite describe stays dry-run-only; computed columns and parameter types are out of scope
- 0006 — migrations are paired up/down SQL files with a timestamp version and a checksummed ledger
- 0007 — MySQL migrations are not transactional, because MySQL commits DDL implicitly
- 0008 — the 1.0 public-API stability commitment, strong-naming, package signing, and the documentation site
bench/SqlBound.Benchmarks compares generated code against Dapper and hand-written ADO.NET.
SqlBound stays within 1–9% of raw ADO.NET and beats Dapper on both time and allocations in every
category measured so far; see docs/benchmarks.md for the methodology and
baseline numbers.
Native AOT and trimming compatibility is exercised on every CI run by
test/SqlBound.AotSmokeTest, a published-and-executed smoke test.
Work proceeds milestone by milestone across six phases, each phase owning a semantic-versioning minor version:
| Phase | Milestones | Status |
|---|---|---|
| 1 — Bedrock | M1–M3 | Done (v0.1.0) |
| 2 — Codegen | M4–M6 | Done (v0.2.0) |
| 3 — Verification | M7–M9 | Done (v0.3.0) |
| 4 — Providers | M10–M12 | Done (v0.4.0) |
| 5 — Migrations & CLI | M13–M15 | Done (v0.5.0) |
| 6 — Ship | M16 | Release candidate (v1.0.0-rc.1) |
Copyright 2026 Luís Amorim. Licensed under the Apache License 2.0.
