Skip to content

Commands take the type in their async terminals (ToArrayAsync<int>(ct)), and Map<int>() reads the first column - #111

Merged
electricessence merged 7 commits into
masterfrom
feat/command-terminals
Nov 11, 2026
Merged

electricessence merged 7 commits into
masterfrom
feat/command-terminals

Conversation

@electricessence

@electricessence electricessence commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

await db.Command(sql).ToArrayAsync<int>(ct) now works, with no .Map(static r => r.GetInt32(0)). Two things make that so:

  • Map<T>() of a single value reads the first column. A primitive, an enum, string, decimal, DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly, Guid, byte[] or a Nullable<> of one is read from column 0 of each row, whatever it is named; other columns are ignored. The column is read as the mapper reads a property of that type: the typed getter when the column type matches (no boxing), range-checked integer narrowing, else Convert. A NULL into a non-nullable T is default(T); int?, string and byte[] get null. This reverses the rc.3 behaviour where Map<int>() threw (whats-new-11 item 9 is rewritten). It is built inside RowMap<T> from the helpers the property mapper already uses (a small internal FirstColumn<T> row source); RowMap<int>.Default, Results<int>(), ToChannel<int>() and the other entry points still throw, with a message that now names Map<T>() and ToListAsync<T>(). DateOnly and TimeOnly were misjudged as row models before (a misleading "N public constructors" message); they are single values now.
  • Six async forwarders on IExecuteReader, each one expression, Map<T>() then the terminal, token last: ToListAsync<T>, ToArrayAsync<T>, ToImmutableArrayAsync<T>, FirstOrDefaultAsync<T>, SingleOrDefaultAsync<T>, ToHashSetAsync<T>(comparer = null, cancellationToken = default). For a row model they equal Map<T>().ToXAsync(). An unmappable T throws at the call, before anything runs.

Owner decision: only the async shortcuts are on the command; the synchronous versions stay at the reader level. There are no sync forwarders, so a sync caller writes command.Map<int>().ToList() (which works because of the first bullet). Nothing that exists changed.

Binding notes (pinned by a compile-only test file): no call that compiled before changes binding. cmd.ToListAsync<int>(default) or (null) on an expressive command still binds to the obsolete instance ToListAsync(transform, behavior); pass the token or nothing. ToHashSetAsync<int>(ct) does not compile (the comparer comes first, as on MappedQuery<T>): write cancellationToken: ct.

One deviation from the plan, which said a reader that reports no columns would give a ValueMappingException: a reader that reports no columns but has a row throws the provider's IndexOutOfRangeException unwrapped. The code is left as it is and SingleValueTerminalTests pins it.

Docs

  • mapping.md: new section "One value per row" (which types, first column, conversion, NULL, read through, how it differs from FirstOrdinalResults<T>() and ExecuteScalar<T>()); the errors table row and the SingleResult example use Map<int>().
  • whats-new-11.md: highlights, NULL rules, a bullet "Commands take the type, asynchronously", and upgrading item 9 rewritten (Map<int>() reads the first column; 10.x returned default(T)).
  • expressive-commands.md (terminals table, which scalar form, a sample), foundation.md, coming-from-dapper.md (Query<int> row, QueryAsync<T>).
  • XML docs on the six members (the full rules on ToListAsync<T>, inherited by the others) and on Map<T>() and RowMap<T>.
  • Every new example is a compiled sample (Mapping.cs, ExpressiveCommands.cs) with checks in Program.cs.

Tests

  • SingleValueTerminalTests: every single-value type from a column of its own type; NULL in first and later rows; conversions (long to int, overflow as ValueMappingException, decimal, text, enum, Guid); extra columns and column order; empty results; async parity with Map<T>() for int, int?, string and a row model; an unmappable T throws at the call with nothing executed; read through later result sets and .SingleResult(); token combining; a DbDataReader and a plain IDataReader; the per-result-set cache keyed by column type; a column that changes type mid-set.
  • SingleValueBindingTests: a compile-only table of every call form, plus the (default) trap.
  • SingleValueAllocationTests: Map<int>() and ToListAsync<int>() allocate no more than the hand-written selector (no per-row boxing).
  • UnmappableTypeTests: single values now pass Map<T>(); the six async terminals join the "every entry point" theories.

Checks

Master, with #110 and #112, is merged in. The only conflicts were in PublicAPI.Unshipped.txt, where both sides' lines are kept, and in one whats-new-11.md bullet, where master's token wording is kept. From the gate (verify-gate.ps1) on the merged head: the Release and Debug builds have 0 warnings and 0 errors; Core tests: 9508 passed, 0 failed, in Release and in Debug; the samples build with 0 warnings and "All 98 sample checks passed."; style and analyzers on every project, all target frameworks, info level: 0 findings; DocFX: 0 warnings, 0 errors.

The last three commits only reformat the lines this PR adds: one parameter per line, => on its own line, doc comments wrapped at natural breaks. The style analyzer (IDE0011) then asked for braces on two multi-line if/else bodies, in RowMap.cs and UnmappableTypeTests.cs. Every changed file was compared with its previous version: the code, with whitespace removed, and the comment text are unchanged apart from those braces.

Not in this change

Synchronous command shortcuts (owner decision). ToDictionary, Read, Skip/Take on the command. Raw IDbCommand/DbCommand/IDataReader receivers. Half, Int128, BigInteger and object as single values. The benchmark pair (Map(selector) vs Map<int>() vs Dapper) is not run here.

🤖 Generated with Claude Code

electricessence and others added 6 commits November 10, 2026 06:51
…async terminals that take the type

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s of a command and their binding

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ne, => on its own line, doc comments wrapped at natural breaks

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@electricessence
electricessence marked this pull request as ready for review November 10, 2026 16:26
@electricessence
electricessence merged commit 3a9b9bc into master Nov 11, 2026
2 checks passed
@electricessence
electricessence deleted the feat/command-terminals branch November 11, 2026 01:40
electricessence added a commit that referenced this pull request Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant