Skip to content

Commit 4411d88

Browse files
rc.4: Map<T>() on a reader or a raw command (MappedReader<T>, MappedCommand<T>) (#117)
* Map<T>() of a single value reads the first column, and a command has async terminals that take the type Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Tests pin the first-column read of a single value, the async terminals of a command and their binding Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * The docs and samples show Map<int>() and ToArrayAsync<int>(ct) reading one column Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * The command terminals follow the vertical style: one parameter per line, => on its own line, doc comments wrapped at natural breaks Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Braces on the multi-line if and else bodies the vertical style created (IDE0011) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Braces on the if that pairs with the braced else (IDE0011) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * reader.Map<T>() and command.Map<T>(): mapped views over a caller's reader or command A null map, an empty pair list and a default struct behave as documented. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Tests for the reader and command Map views Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Docs and samples for the reader and command Map views Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * PublicAPI: the default constructors of the two views are named for their own types Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Fix the test build and the style findings of the view tests and sources Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Fix the view build on netstandard, the null-map deprecation tests and two view tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Expression body for the Command property (IDE0025) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Blank lines between members, PublicAPI order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Views: dispose the reader with ConfigureAwait(false), a cancelled token gives a cancelled task, document Read() and the CS0121 ambiguity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent e5e2790 commit 4411d88

11 files changed

Lines changed: 2033 additions & 23 deletions

File tree

‎Source/Core/MappedQuery.cs‎

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public static partial class CoreExtensions
2929
/// </para>
3030
/// <para>
3131
/// Pass a <see cref="RowMap{T}"/> to read columns whose names differ from the properties; the overload without a map reads by column name.
32+
/// Columns match a property exactly, else ignoring case, else ignoring case and underscores (<c>author_id</c> feeds <c>AuthorId</c>);
33+
/// the resolved layout is cached per result-set layout.
3234
/// Declare the <see cref="RowMap{T}"/> once and pass it: a map built in the call (a <c>new RowMap&lt;T&gt;(...)</c> or a collection expression in the argument) is rebuilt, and its row reader compiled, on every call
3335
/// (about 29 KB and a reader compile for a one-row read of a 12-property model, against 464 B through a declared map; see
3436
/// <see href="https://open-net-libraries.github.io/Open.Database.Extensions/articles/mapping.html#columns-named-differently">Columns named differently</see>).
@@ -42,20 +44,24 @@ public static partial class CoreExtensions
4244
/// </remarks>
4345
/// <typeparam name="T">The model type to map the values to.</typeparam>
4446
/// <param name="command">The command to read from.</param>
45-
/// <param name="map">The prebuilt <see cref="RowMap{T}"/> to read with.</param>
47+
/// <param name="map">The prebuilt <see cref="RowMap{T}"/> to read with; <see langword="null"/> reads as <c>Map&lt;T&gt;()</c> does (before 11.0.0-rc.4 it threw).</param>
4648
/// <returns>A deferred query whose results are mapped to <typeparamref name="T"/>.</returns>
47-
/// <exception cref="ArgumentNullException"><paramref name="command"/> or <paramref name="map"/> is <see langword="null"/>.</exception>
49+
/// <exception cref="ArgumentNullException"><paramref name="command"/> is <see langword="null"/>.</exception>
4850
/// <seealso cref="RowMap{T}"/>
4951
[OverloadResolutionPriority(1)]
5052
public static MappedQuery<T> Map<T>(
5153
this IExecuteReader command,
52-
RowMap<T> map)
54+
RowMap<T>? map)
5355
{
5456
if (command is null) throw new ArgumentNullException(nameof(command));
55-
if (map is null) throw new ArgumentNullException(nameof(map));
5657
Contract.EndContractBlock();
5758

58-
return new MappedQuery<T>(new MappedQueryCore<T>(command, map, 0, -1, false));
59+
// No map reads exactly as Map<T>() does:
60+
// a single value from the first column, a model by name.
61+
return map is null
62+
? Map<T>(command)
63+
: new MappedQuery<T>(
64+
new MappedQueryCore<T>(command, map, 0, -1, false));
5965
}
6066

6167
/// <inheritdoc cref="Map{T}(IExecuteReader, RowMap{T}?)" path="/*[not(self::param)]"/>
@@ -65,7 +71,9 @@ public static MappedQuery<T> Map<T>(
6571
public static MappedQuery<T> Map<T>(
6672
this IExecuteReader command,
6773
params ReadOnlySpan<KeyValuePair<string, string?>> mappings)
68-
=> Map(command, RowMap<T>.From(mappings));
74+
=> mappings.IsEmpty
75+
? Map<T>(command)
76+
: Map(command, RowMap<T>.From(mappings));
6977

7078
/// <inheritdoc cref="Map{T}(IExecuteReader, RowMap{T}?)" path="/*[not(self::param)]"/>
7179
/// <param name="command">The command to read from.</param>
@@ -123,45 +131,50 @@ readonly struct MappedQueryCore<T>
123131
// A caller-supplied selector may read in any order, so it runs with the default access mode.
124132
const CommandBehavior GeneratedMapping = CommandBehavior.SingleResult | CommandBehavior.SequentialAccess;
125133

126-
readonly IExecuteReader _command;
127134
readonly IRowSource<T> _source;
128135
readonly int _skip;
129136
readonly int _take; // -1 == unbounded.
130137
readonly bool _singleResult; // SingleResult() was called: stop after the first result set and do not read through the rest.
131138

132139
internal MappedQueryCore(IExecuteReader command, IRowSource<T> source, int skip, int take, bool singleResult)
133-
{ _command = command; _source = source; _skip = skip; _take = take; _singleResult = singleResult; }
140+
{ Command = command; _source = source; _skip = skip; _take = take; _singleResult = singleResult; }
141+
142+
// A default struct has no command: any use of it says so,
143+
// the same way for every type of the family.
144+
IExecuteReader Command
145+
=> field ?? throw new InvalidOperationException(
146+
"This query was not created by Map<T>(); a default value cannot be used.");
134147

135148
// Without .SingleResult() the reader is asked for every result set, because a terminal reads through the later ones to report their failures.
136149
CommandBehavior Behavior => _singleResult
137150
? (_source is SelectorSource<T> ? CommandBehavior.SingleResult : GeneratedMapping)
138151
: (_source is SelectorSource<T> ? CommandBehavior.Default : CommandBehavior.SequentialAccess);
139152

140-
internal MappedQueryCore<T> WithSingleResult() => new(_command, _source, _skip, _take, true);
153+
internal MappedQueryCore<T> WithSingleResult() => new(Command, _source, _skip, _take, true);
141154

142155
internal MappedQueryCore<T> WithSkip(int count)
143-
=> count >= 0 ? new(_command, _source, _skip + count, _take, _singleResult)
156+
=> count >= 0 ? new(Command, _source, _skip + count, _take, _singleResult)
144157
: throw new ArgumentOutOfRangeException(nameof(count), count, "Cannot be negative.");
145158

146159
// Take is set exactly once (the staging structs only expose it before any take exists), so this
147160
// just records the limit; it applies after the skip, i.e. Enumerable.Skip(a).Take(b).
148161
internal MappedQueryCore<T> WithTake(int count)
149-
=> count >= 0 ? new(_command, _source, _skip, count, _singleResult)
162+
=> count >= 0 ? new(Command, _source, _skip, count, _singleResult)
150163
: throw new ArgumentOutOfRangeException(nameof(count), count, "Cannot be negative.");
151164

152165
// Honors the command's UseAsyncRead choice (default false: rows are pulled with Read() after an async execute).
153-
bool UseAsyncRead => _command is IExecuteReaderAsync { UseAsyncRead: true };
166+
bool UseAsyncRead => Command is IExecuteReaderAsync { UseAsyncRead: true };
154167

155168
// One token governs a whole call: the one passed to the terminal together with the command's own (either cancels), for the connection open, the name lookup, the execute and the reads.
156169
// No token, one token, or the same token twice needs no source, so those calls allocate nothing for it. Two different tokens need a linked source, which the call disposes when it ends.
157170
CancellationToken Combine(CancellationToken cancellationToken, out CancellationTokenSource? linked)
158-
=> CancellationTokens.Combine(_command.CancellationToken, cancellationToken, out linked);
171+
=> CancellationTokens.Combine(Command.CancellationToken, cancellationToken, out linked);
159172

160173
// `token` is the combined token; it is also in `state`, which the terminal's static lambda reads it from.
161174
ValueTask<TResult> ExecuteAsync<TState, TResult>(TState state, Func<IDataReader, TState, ValueTask<TResult>> transform, CommandBehavior behavior, CancellationTokenSource? linked, CancellationToken token)
162175
=> linked is null
163-
? _command.ExecuteReaderAsync(state, transform, behavior, token)
164-
: ExecuteLinkedAsync(_command, state, transform, behavior, linked, token);
176+
? Command.ExecuteReaderAsync(state, transform, behavior, token)
177+
: ExecuteLinkedAsync(Command, state, transform, behavior, linked, token);
165178

166179
static async ValueTask<TResult> ExecuteLinkedAsync<TState, TResult>(IExecuteReader command, TState state, Func<IDataReader, TState, ValueTask<TResult>> transform, CommandBehavior behavior, CancellationTokenSource linked, CancellationToken token)
167180
{
@@ -173,8 +186,8 @@ static async ValueTask<TResult> ExecuteLinkedAsync<TState, TResult>(IExecuteRead
173186
// Nothing is read through when the transform throws, so the caller's own exception is never replaced.
174187
TResult Run<TState, TResult>(TState state, Func<IDataReader, TState, TResult> transform, CommandBehavior behavior)
175188
=> _singleResult
176-
? _command.ExecuteReader(state, transform, behavior)
177-
: _command.ExecuteReader((state, transform), static (reader, s) =>
189+
? Command.ExecuteReader(state, transform, behavior)
190+
: Command.ExecuteReader((state, transform), static (reader, s) =>
178191
{
179192
TResult result = s.transform(reader, s.state);
180193
RowReader.ReadThrough(reader);
@@ -197,14 +210,14 @@ ValueTask<TResult> RunAsync<TState, TResult>(TState state, Func<IDataReader, TSt
197210
#region Handler (streaming) terminals
198211

199212
internal void Read(Action<IEnumerable<T>> handler)
200-
=> _command.ExecuteReader((handler, _source, _skip, _take, _singleResult), static (reader, s) =>
213+
=> Command.ExecuteReader((handler, _source, _skip, _take, _singleResult), static (reader, s) =>
201214
{
202215
s.handler(RowReader.Stream(s._source, reader, s._skip, s._take, !s._singleResult));
203216
return false;
204217
}, Behavior);
205218

206219
internal TResult Read<TResult>(Func<IEnumerable<T>, TResult> handler)
207-
=> _command.ExecuteReader((handler, _source, _skip, _take, _singleResult), static (reader, s)
220+
=> Command.ExecuteReader((handler, _source, _skip, _take, _singleResult), static (reader, s)
208221
=> s.handler(RowReader.Stream(s._source, reader, s._skip, s._take, !s._singleResult)), Behavior);
209222

210223
internal ValueTask ReadAsync(Func<IAsyncEnumerable<T>, CancellationToken, ValueTask> handler, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)