diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index eade08cb2..ded95c821 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -934,17 +934,26 @@ 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; + DbCommand? cmd = null; + 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 + cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + 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(); + + var deferred = ExecuteReaderSync(results, reader, cmd, cnn, wasClosed); + reader = null; + cmd = null; + wasClosed = false; + return deferred; } finally { + reader?.Dispose(); + cmd?.Dispose(); if (wasClosed) cnn.Close(); } } @@ -983,20 +992,44 @@ 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; + DbCommand? cmd = null; + 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); + cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + 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; - } - finally - { - if (wasClosed) cnn.Close(); + if (command.Buffered) return results.ToList(); + + var deferred = ExecuteReaderSync(results, reader, cmd, cnn, wasClosed); + reader = null; + cmd = null; + wasClosed = false; + return deferred; + } + finally + { + reader?.Dispose(); + cmd?.Dispose(); + if (wasClosed) cnn.Close(); } } + private static IEnumerable ExecuteReaderSync(IEnumerable results, DbDataReader reader, DbCommand command, IDbConnection cnn, bool wasClosed) + { + try + { + foreach (var result in results) yield return result; + } + finally + { + reader.Dispose(); + command.Dispose(); + if (wasClosed) cnn.Close(); + } + } + private static IEnumerable ExecuteReaderSync(DbDataReader reader, Func func, object? parameters) { using (reader) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f91a3cca..b1e42ba41 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,7 @@ + diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index 93b498618..e9eab6b88 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/tests/Dapper.Tests/FakeDbTests.Issue2099.cs b/tests/Dapper.Tests/FakeDbTests.Issue2099.cs new file mode 100644 index 000000000..89887b973 --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Issue2099.cs @@ -0,0 +1,32 @@ +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests; + +public sealed class FakeDbIssue2099Tests +{ + [Fact] + public async Task UnbufferedAsyncMultiMapKeepsReaderAliveUntilEnumeration() + { + using var connection = new fakeDbConnection(new FakeDataStore()); + connection.EnqueueReaderResult(new[] + { + new Dictionary { ["A"] = 1, ["B"] = 2 }, + }); + connection.Open(); + + var command = new CommandDefinition("select 1 as A, 2 as B", flags: CommandFlags.None); + var results = await connection.QueryAsync(command, (a, b) => a.A + b.B, splitOn: "B"); + + Assert.Equal(3, results.Single()); + } + + private sealed class First { public int A { get; set; } } + private sealed class Second { public int B { get; set; } } +} +#endif