Skip to content

RowMap<T>.Convert: a converter for one column, the rest of the type still maps by name - #125

Merged
electricessence merged 7 commits into
masterfrom
feat/rowmap-converter
Nov 12, 2026
Merged

electricessence merged 7 commits into
masterfrom
feat/rowmap-converter

Conversation

@electricessence

Copy link
Copy Markdown
Collaborator

RowMap<T>.Convert: a converter for one column, and the rest of the type still maps by name.

A column that needs a trim, a parse or a format on its way to a member no longer forces a hand-written selector, or a row type that is mapped and then copied.

RowMap<Order> map = RowMap<Order>.Default
    .Convert<long, string>(nameof(Order.Number), n => n.ToString("D6", Invariant))
    .Convert<string, string>(nameof(Order.Customer), c => c.Trim());

Order[] orders = await connection
    .Command("select id, order_num as number, customer from orders")
    .Map(map)
    .ToArrayAsync(ct);   // every other column still maps by name

What

  • Two methods on RowMap<T>, both returning a NEW map (the original is unchanged, as a map is immutable):
    • Convert<TColumn, TMember>(string member, Func<TColumn, TMember> convert): the converter attaches to member; the column is the one the map already names for it, else the column the member matches by name.
    • Convert<TColumn, TMember>(string member, string column, Func<TColumn, TMember> convert): also names the column.
  • The delegate runs inside the compiled row reader, as the value is read, not in a second pass over a row object. When the column's type is TColumn the value comes from the provider's typed getter, so nothing is boxed; otherwise it is read the way Map<int>() reads a single value (an integer converts within its range; an overflow throws ValueMappingException). The only per-row cost is the delegate.
  • It works for properties, constructor parameters, required and init members, in the live-reader path and in the buffered path, and with every way a map is declared.
  • Validated when Convert is called, as pairs are when a map is created: an unknown or unsettable member, a member converted twice, a TMember the member cannot take, a member mapped to a null column, a column another member reads, and null arguments.

Please look at (design choices)

  • NULL. A converter is not called for a NULL cell; the member follows the existing NULL rules (null for a nullable member, its initializer or default for a non-nullable one, the declared default for a constructor parameter). Only a Nullable<> TColumn (int?, DateTime?) is called with null, and its result goes to the member, a non-nullable member included. A reference-type TColumn, even string?, never sees NULL, because the annotation does not exist at run time on a type argument.
  • Overload shape and name. Convert<TColumn, TMember>(member, [column,] convert) with the member by name. A lambda alone cannot infer TColumn, so a caller writes Convert<long, string>(...) or types the lambda parameter. This is documented.
  • A map enumerates pairs, not delegates. The column form shows up as a pair; the name-only form does not, and new RowMap<T>(map) drops converters. Documented.
  • An exception the delegate throws surfaces as a ValueMappingException with the original inside, like any other failure while reading a value (a database exception and a cancellation pass through). Documented.

Docs and samples

  • mapping.md has a "Converting a column" section; the RowMap<T> remarks cover the NULL rule; whats-new-11.md has an entry; a compiled sample shows order_num read as text and a timestamp as "HH:mm". The sample adds an orders table to the shared sample seed.

Tests

  • 50 cases: converter on a named pair, by name only, the explicit-column form; trim, parse and format examples; the rest of the type still maps by name; NULL handling including a nullable TColumn; a column of another type converts and an overflow throws; constructor, required and init members; validation at Convert; immutability of the original map; two result-set layouts; use of one map from several threads; no per-row allocation beyond the delegate; through a reader, a command and a table.

Checks

  • Release and Debug builds: 0 warnings, 0 errors. Tests: 9727 of 9727 pass in both. Samples: 105 of 105 checks pass. Style and analyzers: 0 findings. DocFX: 0 warnings.

🤖 Generated with Claude Code

electricessence and others added 7 commits November 12, 2026 04:19
…till maps by name

Convert<TColumn, TMember>(member, [column,] convert) returns a new RowMap<T>. The converter is compiled into the
row reader and reads the column with the typed getter; NULL skips it unless TColumn is Nullable<>.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…xceptions are wrapped, the member-name wording is exact

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lection expression for the row

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@electricessence
electricessence merged commit 594cc6e into master Nov 12, 2026
2 checks passed
@electricessence
electricessence deleted the feat/rowmap-converter branch November 12, 2026 15:31
electricessence added a commit that referenced this pull request Nov 12, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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