Skip to content

Avoid double-enumerating IEnumerable<T> in AsTableValuedParameter#2217

Open
hexonal wants to merge 3 commits into
DapperLib:mainfrom
hexonal:fix-tvp-double-enumeration
Open

Avoid double-enumerating IEnumerable<T> in AsTableValuedParameter#2217
hexonal wants to merge 3 commits into
DapperLib:mainfrom
hexonal:fix-tvp-double-enumeration

Conversation

@hexonal

@hexonal hexonal commented Jul 15, 2026

Copy link
Copy Markdown

SqlDataRecordListTVPParameter<T>.Set used data.Any() to swap an empty sequence for null, which forced an extra enumeration of the source before the caller ever executed the command. For single-pass sources (open readers, streaming iterators, etc.) that meant the real enumeration during command execution either restarted from scratch or failed outright with something like "There is already an open DataReader...".

This now only short-circuits to null when the source is a known IReadOnlyCollection<T>, checked via Count as @mgravell suggested in the issue thread. Everything else flows straight through untouched, so it's enumerated exactly once, by whoever actually executes the command.

Added three tests: one mirrors the original repro end-to-end (needs SQL Server to run), and two run offline against SqlDataRecordListTVPParameter<T>.Set directly, checking that a non-collection source is never touched and a known-empty collection still nulls out.

Fixes #2064

SqlDataRecordListTVPParameter<T>.Set used data.Any() to swap an empty
sequence for null, which forced an extra enumeration of the source
before the caller ever executed the command. For single-pass sources
(open readers, streaming iterators, etc.) the real enumeration during
command execution then either restarted from scratch or failed
outright.

Only short-circuit to null when the source is a known
IReadOnlyCollection<T> and we can check Count for free; anything else
now flows straight through untouched, so it's enumerated exactly once
by the provider.

Fixes DapperLib#2064
The two new offline tests hardcoded Microsoft.Data.SqlClient.SqlParameter
even when running under the SystemSqlClientParameterTests variant,
unlike the rest of this file which uses Provider.CreateRawParameter to
stay provider-agnostic across the two test subclasses.
TestSqlDataRecordListParametersWithAsTableValuedParameterSinglePassSource
was failing against real SQL Server with:

  InvalidCastException: Failed to convert parameter value from a
  SingleEnumerationEnumerable`1 to a IEnumerable`1. Object must
  implement IConvertible.

SqlParameter.CoerceValue recognizes a Structured/TVP value only when
it is a DataTable, a DbDataReader, or satisfies
"value is IEnumerable<SqlDataRecord>" - a hard runtime interface
check, not an assignment-compatibility check. Generic interface
implementations are invariant on their concrete type parameter even
though IEnumerable<T> itself is declared covariant, so a wrapper
class instantiated as SingleEnumerationEnumerable<IDataRecord> never
satisfies that check, no matter what concrete objects it yields.
Everything else falls through to CoerceValue's scalar
Convert.ChangeType path, which is exactly the IConvertible error
above.

The test was wrapping the shared, provider-agnostic
IEnumerable<IDataRecord> helper, so AsTableValuedParameter<T> always
inferred T as IDataRecord and the wrapper only ever implemented
IEnumerable<IDataRecord>. This has nothing to do with the
double-enumeration fix itself; SqlDataRecordListTVPParameter<T>.Set
still only enumerates its source once either way.

Wrap the provider-specific SqlDataRecord list directly instead,
matching the branching already used in
TestSqlDataRecordListParametersWithTypeHandlers, so the wrapper's
concrete type parameter is the real SqlDataRecord type SqlClient is
checking for.
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.

Multiple enumerations of IEnumerable<SqlDataRecord> passed as TVP

2 participants