Skip to content

Fix MultiMapAsync disposing the reader before unbuffered results are enumerated - #2229

Open
jinseojang0903 wants to merge 1 commit into
DapperLib:mainfrom
jinseojang0903:fix/multimapasync-unbuffered-reader-dispose
Open

jinseojang0903 wants to merge 1 commit into
DapperLib:mainfrom
jinseojang0903:fix/multimapasync-unbuffered-reader-dispose

Conversation

@jinseojang0903

Copy link
Copy Markdown

Fixes #2099.

Problem

When calling any multi-map QueryAsync overload (e.g. QueryAsync<TFirst, TSecond, TReturn> or the Type[]-based QueryAsync<TReturn>)
with buffered: false, enumerating the returned sequence throws once the reader has already been disposed:

System.InvalidOperationException: Invalid attempt to call FieldCount when reader is closed.
at Dapper.SqlMapper.GetColumnHash(...)
at Dapper.SqlMapper.MultiMapImpl[...]+MoveNext()

Root cause

Both MultiMapAsync<TFirst,...,TSeventh,TReturn> and MultiMapAsync<TReturn> (Type[] overload) in SqlMapper.Async.cs wrapped the
DbDataReader in a using block. MultiMapImpl builds its result as a lazy, yield return-based IEnumerable<TReturn> that doesn't
start executing until the caller enumerates it. For buffered: false, the async method returns that un-enumerated sequence directly — so
the using disposes the reader immediately on return, before the caller ever gets a chance to read from it.

The single-type QueryAsync<T> path already avoids this by deferring disposal: it hands the reader into a small iterator
(ExecuteReaderSync) that only disposes it once the caller finishes enumerating. This PR applies the same pattern to both multi-map
overloads, adding a matching IEnumerable<TReturn>-based overload of ExecuteReaderSync for them to share.

Fix

  • SqlMapper.Async.cs: transfer reader ownership into the deferred sequence instead of disposing it unconditionally when buffered: false.
  • No change to the buffered: true path (already worked correctly, since .ToList() fully consumes the reader before the method returns).

Tests

Added two regression tests in AsyncTests.cs, following the existing TestMultiMapWithSplitAsync / TestMultiMapArbitraryWithSplitAsync
style:

  • TestMultiMapWithSplitUnbufferedAsync
  • TestMultiMapArbitraryWithSplitUnbufferedAsync

Both fail with the reported exception before this fix and pass after it. Full suite run locally (net8.0 + net10.0, SQL
Server/MySQL/Postgres) with no regressions.

…enumerated

Both the fixed-arity and Type[]-based MultiMapAsync overloads wrapped the
DbDataReader in a `using` that disposed it as soon as the async method
returned. For buffered:false calls the returned IEnumerable<TReturn> is a
lazy (yield-based) sequence that hasn't started executing yet, so the reader
was already closed by the time the caller enumerated it, throwing
ObjectDisposedException / "reader is closed" on first access.

Mirrors the disposal-deferral pattern already used by the single-type
QueryAsync<T> unbuffered path: transfer reader ownership into the returned
sequence instead of disposing it unconditionally.

Fixes DapperLib#2099

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPzJgxak5nZ4u3j88ui5xX
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.

MultiMapAsync disposes reader in unbuffered query

1 participant