diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index eade08cb2..575947fb9 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -934,17 +934,25 @@ private static async Task> MultiMapAsync(command.CommandText, command.CommandTypeDirect, cnn, typeof(TFirst), param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + DbDataReader? reader = null; try { if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); - using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); - using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); - if (!command.Buffered) wasClosed = false; // handing back open reader; rely on command-behavior + reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); var results = MultiMapImpl(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; + if (command.Buffered) + { + return results.ToList(); + } + wasClosed = false; // handing back open reader; rely on command-behavior + var deferred = ExecuteReaderSync(reader, results); + reader = null; // to prevent it being disposed before the caller gets to see it + return deferred; } finally { + using (reader) { /* dispose if non-null */ } if (wasClosed) cnn.Close(); } } @@ -983,16 +991,25 @@ private static async Task> MultiMapAsync(this IDbC var identity = new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn, types[0], param?.GetType(), types); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + DbDataReader? reader = null; try { if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); - using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); - using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); + reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); var results = MultiMapImpl(null, default, types, map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; + if (command.Buffered) + { + return results.ToList(); + } + wasClosed = false; // handing back open reader; rely on command-behavior + var deferred = ExecuteReaderSync(reader, results); + reader = null; // to prevent it being disposed before the caller gets to see it + return deferred; } finally { + using (reader) { /* dispose if non-null */ } if (wasClosed) cnn.Close(); } } @@ -1010,6 +1027,21 @@ private static IEnumerable ExecuteReaderSync(DbDataReader reader, Func ExecuteReaderSync(DbDataReader reader, IEnumerable results) + { + using (reader) + { + foreach (var item in results) + { + yield return item; + } + } + } + /// /// Execute a command that returns multiple result sets, and access each in turn. /// diff --git a/tests/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs index 9c3ec4721..e924b6ee4 100644 --- a/tests/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -300,6 +300,26 @@ public async Task TestMultiMapWithSplitAsync() Assert.Equal("def", product.Category.Name); } + [Fact] + public async Task TestMultiMapWithSplitUnbufferedAsync() + { + const string sql = "select 1 as id, 'abc' as name, 2 as id, 'def' as name"; + var productQuery = await connection.QueryAsync(sql, (prod, cat) => + { + prod.Category = cat; + return prod; + }, buffered: false).ConfigureAwait(false); + + // the reader must still be alive when we start enumerating, even though + // the QueryAsync call above has already completed + var product = productQuery.First(); + Assert.Equal(1, product.Id); + Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); + Assert.Equal(2, product.Category.Id); + Assert.Equal("def", product.Category.Name); + } + [Fact] public async Task TestMultiMapArbitraryWithSplitAsync() { @@ -320,6 +340,27 @@ public async Task TestMultiMapArbitraryWithSplitAsync() Assert.Equal("def", product.Category.Name); } + [Fact] + public async Task TestMultiMapArbitraryWithSplitUnbufferedAsync() + { + const string sql = "select 1 as id, 'abc' as name, 2 as id, 'def' as name"; + var productQuery = await connection.QueryAsync(sql, new[] { typeof(Product), typeof(Category) }, (objects) => + { + var prod = (Product)objects[0]; + prod.Category = (Category)objects[1]; + return prod; + }, buffered: false).ConfigureAwait(false); + + // the reader must still be alive when we start enumerating, even though + // the QueryAsync call above has already completed + var product = productQuery.First(); + Assert.Equal(1, product.Id); + Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); + Assert.Equal(2, product.Category.Id); + Assert.Equal("def", product.Category.Name); + } + [Fact] public async Task TestMultiMapWithSplitClosedConnAsync() {