diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt index cf9343ccf..57e2f2d9f 100644 --- a/Dapper/PublicAPI.Unshipped.txt +++ b/Dapper/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool -static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void \ No newline at end of file +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Type![]! types, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! \ No newline at end of file diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index eade08cb2..9176441cc 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -448,7 +448,6 @@ private static async Task> QueryAsync(this IDbConnection cnn, if (command.Buffered) { var buffer = new List(); - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { object val = func(reader); @@ -949,6 +948,20 @@ private static async Task> MultiMapAsync + /// Perform an asynchronous multi-mapping query with an arbitrary number of input types. + /// This returns a single type, combined from the raw types via . + /// + /// The combined type to return. + /// The connection to query on. + /// The command used to execute the query. + /// Array of types in the recordset. + /// The function to map row types to the return type. + /// The field we should split and read the second object from (default: "Id"). + /// An enumerable of . + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Type[] types, Func map, string splitOn = "Id") => + MultiMapAsync(cnn, command, types, map, splitOn); + /// /// Perform an asynchronous multi-mapping query with an arbitrary number of input types. /// This returns a single type, combined from the raw types via . @@ -1301,6 +1314,7 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com bool wasClosed = cnn.State == ConnectionState.Closed; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); DbDataReader? reader = null; + bool readerDrained = false; try { if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); @@ -1312,6 +1326,7 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com { if (reader.FieldCount == 0) { + readerDrained = true; yield break; } tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); @@ -1320,20 +1335,20 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com var func = tuple.Func; - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { object val = func(reader); yield return GetValue(reader, effectiveType, val); } while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } + readerDrained = true; command.OnCompleted(); } finally { if (reader is not null) { - if (!reader.IsClosed) + if (!readerDrained && !reader.IsClosed) { try { cmd?.Cancel(); } catch { /* don't spoil any existing exception */ } diff --git a/tests/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs index 9c3ec4721..23773ab58 100644 --- a/tests/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -931,6 +931,53 @@ public async Task TestMultiMapArbitraryMapsAsync() } } + [Fact] + public async Task TestMultiMapArbitraryMapsCommandDefinitionAsync() + { + const string createSql = @" + create table #ReviewBoards (Id int, Name varchar(20), User1Id int, User2Id int, User3Id int, User4Id int, User5Id int, User6Id int, User7Id int, User8Id int, User9Id int) + create table #Users (Id int, Name varchar(20)) + + insert #Users values(9, 'User 9') + insert #ReviewBoards values(1, 'Review Board 1', 9, 9, 9, 9, 9, 9, 9, 9, 9) +"; + await connection.ExecuteAsync(createSql).ConfigureAwait(false); + try + { + const string sql = @" + select rb.Id, rb.Name, u1.*, u2.*, u3.*, u4.*, u5.*, u6.*, u7.*, u8.*, u9.* + from #ReviewBoards rb + inner join #Users u1 on u1.Id = rb.User1Id + inner join #Users u2 on u2.Id = rb.User2Id + inner join #Users u3 on u3.Id = rb.User3Id + inner join #Users u4 on u4.Id = rb.User4Id + inner join #Users u5 on u5.Id = rb.User5Id + inner join #Users u6 on u6.Id = rb.User6Id + inner join #Users u7 on u7.Id = rb.User7Id + inner join #Users u8 on u8.Id = rb.User8Id + inner join #Users u9 on u9.Id = rb.User9Id"; + + var types = new[] { typeof(ReviewBoard), typeof(User), typeof(User), typeof(User), typeof(User), typeof(User), typeof(User), typeof(User), typeof(User), typeof(User) }; + Func mapper = objects => + { + var board = (ReviewBoard)objects[0]; + board.User9 = (User)objects[9]; + return board; + }; + + using var cts = new CancellationTokenSource(); + var command = new CommandDefinition(sql, cancellationToken: cts.Token); + var data = (await connection.QueryAsync(command, types, mapper).ConfigureAwait(false)).ToList(); + + Assert.Equal(1, data.Count); + Assert.Equal("User 9", data[0].User9!.Name); + } + finally + { + connection.Execute("drop table #Users drop table #ReviewBoards"); + } + } + [Fact] public async Task Issue157_ClosedReaderAsync() {