M2: execution core (SqlSession, SqlParameters)#2
Merged
Conversation
Codifies the practice established while wrapping M1: each roadmap milestone gets a matching GitHub milestone, created per phase, with its PR associated on creation and the milestone closed on merge.
.editorconfig already declares end_of_line = lf for every file, but nothing enforced it at the git layer. On Windows with core.autocrlf=true (the common default), a fresh checkout converts every committed LF file to CRLF in the working tree, which then fails dotnet format's ENDOFLINE check on every file for any Windows contributor. Normalizing at the git level makes checkouts match the editorconfig policy regardless of the contributor's local autocrlf setting.
xunit 2.x is the legacy package line. Swaps it for xunit.v3 (3.2.2) while keeping Microsoft.NET.Test.Sdk and xunit.runner.visualstudio (bumped to 3.1.5) so execution stays on VSTest via `dotnet test` — no change to the CI workflow or how tests are run. The existing smoke test needed no code changes; namespace and Fact/Assert APIs are unchanged in v3.
The first runtime primitive of the execution core, and the first production code in SqlBound. Public (not internal) since generated code from M4 onward will construct these directly in the consumer's assembly. Normalizes null values to DBNull.Value at construction so downstream ADO.NET binding never needs a separate null check, and rejects empty/whitespace/duplicate names to fail fast on caller bugs rather than silently misbinding a query parameter.
The Async-suffix naming rule added in M1 didn't account for testing.md's Should_ExpectedOutcome_When_Scenario convention, which async test methods must also follow. This is the first commit with async test methods, so it's the first time the two rules conflicted. Scopes the async-suffix rule off for test/**/*.cs, where the TDD naming convention takes precedence.
SqlSession is the instance-based entry point for the coexistence-safe dynamic surface (never named Query*/Execute*/ExecuteScalar* on DbConnection, per the Dapper-coexistence constraint). It never opens, closes, or owns the connection it's given — it requires an already-open one and fails fast with InvalidOperationException otherwise, so behavior can't surprise a caller sharing the connection with Dapper. RunAsync executes a non-query statement and returns the affected row count. Cancellation is checked before any ADO.NET call is made (a pre-canceled token throws before a command is even created) and the token is also threaded through to ExecuteNonQueryAsync so cancellation during execution propagates too. Provider exceptions are left unwrapped. FakeDbConnection/FakeDbCommand/FakeDbParameter(Collection) are minimal hand-rolled subclasses of the abstract System.Data.Common types, chosen over a real provider (e.g. SQLite) to keep unit tests hermetic and defer the real-provider question to M3, which needs one regardless for the Dapper-coexistence sample.
Executes a scalar-returning statement and converts the boxed ADO.NET result to T. A database null becomes default(T) when T can represent it (a reference type or Nullable<T>); for a non-nullable value type it throws InvalidOperationException instead, since there's no way to represent "no value" there. Falls back to Convert.ChangeType for same-family conversions (e.g. a provider returning long for a query the caller expects as int) after a fast exact-type-match path. Extracts the sql/cancellation/connection-state validation shared with RunAsync into ValidateAndPrepare, now that two methods need it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Milestone 2 (Phase 1 — Bedrock) of the SqlBound roadmap: the hand-written runtime primitives that both manual callers and the future source generator (M4+) will call into.
SqlParameters: an immutable, public, named parameter set. Normalizes null values toDBNull.Valueat construction so downstream ADO.NET binding never needs a separate null check; rejects empty/whitespace/duplicate names.SqlSession: the coexistence-safe, instance-based entry point for the "dynamic surface" (deliberately not namedQuery*/Execute*/ExecuteScalar*onDbConnection, per the Dapper-coexistence design constraint). Never opens, closes, or owns the connection — requires an already-open one, fails fast withInvalidOperationExceptionotherwise.RunAsync: non-query execution, returns affected row count.FetchScalarAsync<T>: scalar execution withDBNull/null-aware conversion toT(throws for non-nullable value types,defaultfor nullable ones), falling back toConvert.ChangeTypefor same-family type mismatches (e.g. a provider returninglongwhereintwas requested).FakeDbConnection/FakeDbCommand/FakeDbParameter(Collection)) in the test project, chosen over a real provider to keep unit tests hermetic — the real-provider question is deferred to M3, which needs one regardless for the Dapper-coexistence sample.Two infrastructure fixes surfaced along the way and landed as their own commits, ahead of the feature work that exposed them:
.gitattributes:.editorconfigalready declaredend_of_line = lf, but nothing enforced it at the git layer — on Windows withcore.autocrlf=true(a common default), every fresh checkout converted committed LF to CRLF, failingdotnet formaton every file. Now normalized at the git level..editorconfignaming rule requiring anAsyncsuffix on async methods didn't account for testing.md'sShould_ExpectedOutcome_When_Scenarioconvention, which async test methods must also follow. Scoped off fortest/**/*.cs.Also upgraded the test project from the legacy
xunit2.x package toxunit.v3, keeping VSTest execution (Microsoft.NET.Test.Sdk+xunit.runner.visualstudio) sodotnet testand CI are unaffected.Why
These are the primitives every later milestone builds on — M4's source generator will emit calls into
SqlSession, andSqlParametersis public specifically because generated code compiles into the consumer's assembly.Test plan
SqlParameters,SqlSession.RunAsync,SqlSession.FetchScalarAsync<T>dotnet build— 0 warnings, 0 errors (TreatWarningsAsErrors+EnforceCodeStyleInBuild)dotnet format --verify-no-changes— cleandotnet pack— succeedsReviewer notes
SqlSessionrequiring an already-open connection (no auto-open/close) is deliberate, not an oversight — see the Dapper-coexistence constraint in CLAUDE.md.ConvertScalar<T>'sdefault(T) is not nullcheck is the standard idiom for "is T a non-nullable value type" at runtime; used to decide throw-vs-return-default for a null/DBNull scalar result.🤖 Generated with Claude Code