From 03315450d607120cf7b0b248e2815e1ae3ab60ad Mon Sep 17 00:00:00 2001 From: Vyacheslav Date: Tue, 7 Feb 2023 00:02:50 +0300 Subject: [PATCH 1/4] Queries have been rewritten from pseudo-asynchronous to real asynchronous. #1239 --- Dapper.Rainbow/Dapper.Rainbow.csproj | 7 +- Dapper.Rainbow/Database.Async.cs | 8 +- Dapper.StrongName/Dapper.StrongName.csproj | 8 +- Dapper/Dapper.csproj | 8 +- Dapper/SqlMapper.Async.cs | 62 +++++--- tests/Dapper.Tests/AsyncTests.cs | 151 +++++++++++++++----- tests/Dapper.Tests/Dapper.Tests.csproj | 1 + tests/Dapper.Tests/MiscTests.cs | 10 +- tests/Dapper.Tests/ProcedureTests.cs | 3 +- tests/Dapper.Tests/Providers/SqliteTests.cs | 10 +- 10 files changed, 193 insertions(+), 75 deletions(-) diff --git a/Dapper.Rainbow/Dapper.Rainbow.csproj b/Dapper.Rainbow/Dapper.Rainbow.csproj index dcf0f4e40..6e40fe5f8 100644 --- a/Dapper.Rainbow/Dapper.Rainbow.csproj +++ b/Dapper.Rainbow/Dapper.Rainbow.csproj @@ -6,17 +6,18 @@ Trivial micro-orm implemented on Dapper, provides with CRUD helpers. Sam Saffron 2017 Sam Saffron - net461;netstandard2.0;net5.0 + net48;netstandard2.1;net5.0 false + - + - + \ No newline at end of file diff --git a/Dapper.Rainbow/Database.Async.cs b/Dapper.Rainbow/Database.Async.cs index cdbf7e41f..67fd8e0df 100644 --- a/Dapper.Rainbow/Database.Async.cs +++ b/Dapper.Rainbow/Database.Async.cs @@ -26,7 +26,7 @@ public partial class Table string colsParams = string.Join(",", paramNames.Select(p => "@" + p)); var sql = "set nocount on insert " + TableName + " (" + cols + ") values (" + colsParams + ") select cast(scope_identity() as int)"; - return (await database.QueryAsync(sql, o).ConfigureAwait(false)).Single(); + return await database.QueryAsync(sql, o).SingleAsync().ConfigureAwait(false); } /// @@ -77,7 +77,7 @@ public virtual Task FirstAsync() => /// Asynchronously gets the all rows from this table. /// /// Data from all table rows. - public Task> AllAsync() => + public IAsyncEnumerable AllAsync() => database.QueryAsync("select * from " + TableName); } @@ -97,7 +97,7 @@ public Task ExecuteAsync(string sql, dynamic param = null) => /// The SQL to execute. /// The parameters to use. /// An enumerable of for the rows fetched. - public Task> QueryAsync(string sql, dynamic param = null) => + public IAsyncEnumerable QueryAsync(string sql, dynamic param = null) => _connection.QueryAsync(sql, param as object, _transaction, _commandTimeout); /// @@ -194,7 +194,7 @@ public Task> QueryAsyncThe SQL to execute. /// The parameters to use. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public Task> QueryAsync(string sql, dynamic param = null) => + public IAsyncEnumerable QueryAsync(string sql, dynamic param = null) => _connection.QueryAsync(sql, param as object, _transaction); /// diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index 023a4eae4..e32d04d9b 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -5,7 +5,7 @@ Dapper (Strong Named) A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0 + net461;netstandard2.1;net5.0 true true @@ -18,7 +18,11 @@ $(DefineConstants);PLAT_NO_REMOTING;PLAT_SKIP_LOCALS_INIT true - + + + + + diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index 25f87ccf6..e42e436a2 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -5,7 +5,7 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0 + net461;netstandard2.1;net5.0 @@ -15,7 +15,11 @@ $(DefineConstants);PLAT_NO_REMOTING;PLAT_SKIP_LOCALS_INIT true - + + + + + diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index dba737fc6..080728ea7 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -22,7 +22,7 @@ public static partial class SqlMapper /// The command timeout (in seconds). /// The type of command to execute. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static Task> QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); /// @@ -31,7 +31,7 @@ public static Task> QueryAsync(this IDbConnection cnn, stri /// The connection to query on. /// The command used to query on this connection. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command) => + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, CommandDefinition command) => QueryAsync(cnn, typeof(DapperRow), command); /// @@ -84,7 +84,7 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Co /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static Task> QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => QueryAsync(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); /// @@ -198,7 +198,7 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, st /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task> QueryAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { if (type == null) throw new ArgumentNullException(nameof(type)); return QueryAsync(cnn, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); @@ -279,7 +279,7 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Typ /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command) => + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, CommandDefinition command) => QueryAsync(cnn, typeof(T), command); /// @@ -288,7 +288,7 @@ public static Task> QueryAsync(this IDbConnection cnn, Command /// The connection to query on. /// The type to return. /// The command used to query on this connection. - public static Task> QueryAsync(this IDbConnection cnn, Type type, CommandDefinition command) => + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, Type type, CommandDefinition command) => QueryAsync(cnn, type, command); /// @@ -403,7 +403,7 @@ private static DbCommand TrySetupAsyncCommand(this CommandDefinition command, ID } } - private static async Task> QueryAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) + private static async IAsyncEnumerable QueryAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) { object param = command.Parameters; var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); @@ -412,6 +412,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, var cancel = command.CancellationToken; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); DbDataReader reader = null; + try { if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); @@ -422,7 +423,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, if (tuple.Func == null || tuple.Hash != hash) { if (reader.FieldCount == 0) - return Enumerable.Empty(); + yield break; tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); if (command.AddToCache) SetQueryCache(identity, info); } @@ -433,27 +434,52 @@ private static async Task> QueryAsync(this IDbConnection cnn, { var buffer = new List(); var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; - while (await reader.ReadAsync(cancel).ConfigureAwait(false)) + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER) + await using (reader) +#else + using (reader) +#endif { - object val = func(reader); - buffer.Add(GetValue(reader, effectiveType, val)); + while (await reader.ReadAsync(cancel).ConfigureAwait(false)) + { + object val = func(reader); + buffer.Add(GetValue(reader, effectiveType, val)); + } + while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } } - while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } + + reader = null; command.OnCompleted(); - return buffer; + if (wasClosed) + { + cnn.Close(); + wasClosed = false; + } + + foreach (var item in buffer) + { + yield return item; + } } else { - // can't use ReadAsync / cancellation; but this will have to do - wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior - var deferred = ExecuteReaderSync(reader, func, command.Parameters); - reader = null; // to prevent it being disposed before the caller gets to see it - return deferred; + 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 */ } + command.OnCompleted(); } } finally { +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER) + await using (reader) { /* dispose if non-null */ } +#else using (reader) { /* dispose if non-null */ } +#endif if (wasClosed) cnn.Close(); } } diff --git a/tests/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs index 5d24512be..5ca99517f 100644 --- a/tests/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -40,8 +40,11 @@ public abstract class AsyncTests : TestBase where TProvide [Fact] public async Task TestBasicStringUsageAsync() { - var query = await connection.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); - var arr = query.ToArray(); + var arr = + await connection.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ToArrayAsync() + .ConfigureAwait(false); + Assert.Equal(new[] { "abc", "def" }, arr); } @@ -104,8 +107,10 @@ public async Task TestBasicStringUsageQuerySingleOrDefaultAsyncDynamic() [Fact] public async Task TestBasicStringUsageAsyncNonBuffered() { - var query = await connection.QueryAsync(new CommandDefinition("select 'abc' as [Value] union all select @txt", new { txt = "def" }, flags: CommandFlags.None)).ConfigureAwait(false); - var arr = query.ToArray(); + var arr = + await connection.QueryAsync(new CommandDefinition("select 'abc' as [Value] union all select @txt", new { txt = "def" }, flags: CommandFlags.None)) + .ToArrayAsync() + .ConfigureAwait(false); Assert.Equal(new[] { "abc", "def" }, arr); } @@ -113,10 +118,10 @@ public async Task TestBasicStringUsageAsyncNonBuffered() public void TestLongOperationWithCancellation() { CancellationTokenSource cancel = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - var task = connection.QueryAsync(new CommandDefinition("waitfor delay '00:00:10';select 1", cancellationToken: cancel.Token)); + var enumerator = connection.QueryAsync(new CommandDefinition("waitfor delay '00:00:10';select 1", cancellationToken: cancel.Token)).GetAsyncEnumerator(); try { - if (!task.Wait(TimeSpan.FromSeconds(7))) + if (!enumerator.MoveNextAsync().AsTask().Wait(TimeSpan.FromSeconds(7))) { throw new TimeoutException(); // should have cancelled } @@ -132,8 +137,11 @@ public async Task TestBasicStringUsageClosedAsync() { using (var conn = GetClosedConnection()) { - var query = await conn.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); - var arr = query.ToArray(); + var arr = + await conn.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ToArrayAsync() + .ConfigureAwait(false); + Assert.Equal(new[] { "abc", "def" }, arr); } } @@ -141,7 +149,7 @@ public async Task TestBasicStringUsageClosedAsync() [Fact] public async Task TestQueryDynamicAsync() { - var row = (await connection.QueryAsync("select 'abc' as [Value]").ConfigureAwait(false)).Single(); + var row = await connection.QueryAsync("select 'abc' as [Value]").SingleAsync().ConfigureAwait(false); string value = row.Value; Assert.Equal("abc", value); } @@ -149,8 +157,11 @@ public async Task TestQueryDynamicAsync() [Fact] public async Task TestClassWithStringUsageAsync() { - var query = await connection.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); - var arr = query.ToArray(); + var arr = + await connection.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ToArrayAsync() + .ConfigureAwait(false); + Assert.Equal(new[] { "abc", "def" }, arr.Select(x => x.Value)); } @@ -344,9 +355,17 @@ private static async Task LiteralReplacement(IDbConnection conn) await conn.ExecuteAsync("insert literal1 (id,foo) values ({=id}, @foo)", new { id = 123, foo = 456 }).ConfigureAwait(false); var rows = new[] { new { id = 1, foo = 2 }, new { id = 3, foo = 4 } }; await conn.ExecuteAsync("insert literal1 (id,foo) values ({=id}, @foo)", rows).ConfigureAwait(false); - var count = (await conn.QueryAsync("select count(1) from literal1 where id={=foo}", new { foo = 123 }).ConfigureAwait(false)).Single(); + var count = + await conn.QueryAsync("select count(1) from literal1 where id={=foo}", new { foo = 123 }) + .SingleAsync() + .ConfigureAwait(false); + Assert.Equal(1, count); - int sum = (await conn.QueryAsync("select sum(id) + sum(foo) from literal1").ConfigureAwait(false)).Single(); + int sum = + await conn.QueryAsync("select sum(id) + sum(foo) from literal1") + .SingleAsync() + .ConfigureAwait(false); + Assert.Equal(sum, 123 + 456 + 1 + 2 + 3 + 4); } @@ -373,7 +392,11 @@ private static async Task LiteralReplacementDynamic(IDbConnection conn) args = new DynamicParameters(); args.Add("foo", 123); - var count = (await conn.QueryAsync("select count(1) from literal2 where id={=foo}", args).ConfigureAwait(false)).Single(); + var count = + await conn.QueryAsync("select count(1) from literal2 where id={=foo}", args) + .SingleAsync() + .ConfigureAwait(false); + Assert.Equal(1, count); } @@ -386,8 +409,14 @@ await connection.ExecuteAsync("insert #literalin (id) values (@id)", new[] { new { id = 2 }, new { id = 3 }, }).ConfigureAwait(false); - var count = (await connection.QueryAsync("select count(1) from #literalin where id in {=ids}", - new { ids = new[] { 1, 3, 4 } }).ConfigureAwait(false)).Single(); + var count = + await connection.QueryAsync( + "select count(1) from #literalin where id in {=ids}", + new { ids = new[] { 1, 3, 4 } } + ) + .SingleAsync() + .ConfigureAwait(false); + Assert.Equal(2, count); } @@ -435,7 +464,10 @@ public async Task TypeBasedViaTypeAsync() { Type type = Common.GetSomeType(); - dynamic actual = (await MarsConnection.QueryAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).ConfigureAwait(false)).FirstOrDefault(); + dynamic actual = + await MarsConnection.QueryAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }) + .FirstOrDefaultAsync() + .ConfigureAwait(false); Assert.Equal(((object)actual).GetType(), type); int a = actual.A; string b = actual.B; @@ -478,7 +510,10 @@ public async Task Issue22_ExecuteScalarAsync() [Fact] public async Task Issue346_QueryAsyncConvert() { - int i = (await connection.QueryAsync("Select Cast(123 as bigint)").ConfigureAwait(false)).First(); + int i = + await connection.QueryAsync("Select Cast(123 as bigint)") + .FirstAsync() + .ConfigureAwait(false); Assert.Equal(123, i); } @@ -553,13 +588,16 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Default p.Output(bob, b => b.Address.Name); p.Output(bob, b => b.Address.PersonId); - var result = (await connection.QueryAsync(@" + var result = await connection + .QueryAsync(@" SET @Occupation = 'grillmaster' SET @PersonId = @PersonId + 1 SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' SET @AddressPersonId = @PersonId -select 42", p).ConfigureAwait(false)).Single(); +select 42", p) + .SingleAsync() + .ConfigureAwait(false); Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); @@ -581,13 +619,16 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Buffere p.Output(bob, b => b.Address.Name); p.Output(bob, b => b.Address.PersonId); - var result = (await connection.QueryAsync(new CommandDefinition(@" + var result = await connection + .QueryAsync(new CommandDefinition(@" SET @Occupation = 'grillmaster' SET @PersonId = @PersonId + 1 SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' SET @AddressPersonId = @PersonId -select 42", p, flags: CommandFlags.Buffered)).ConfigureAwait(false)).Single(); +select 42", p, flags: CommandFlags.Buffered)) + .SingleAsync() + .ConfigureAwait(false); Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); @@ -609,13 +650,16 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_NonBuff p.Output(bob, b => b.Address.Name); p.Output(bob, b => b.Address.PersonId); - var result = (await connection.QueryAsync(new CommandDefinition(@" + var result = await connection + .QueryAsync(new CommandDefinition(@" SET @Occupation = 'grillmaster' SET @PersonId = @PersonId + 1 SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' SET @AddressPersonId = @PersonId -select 42", p, flags: CommandFlags.None)).ConfigureAwait(false)).Single(); +select 42", p, flags: CommandFlags.None)) + .SingleAsync() + .ConfigureAwait(false); Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); @@ -663,22 +707,40 @@ select 17 [Fact] public async Task TestSubsequentQueriesSuccessAsync() { - var data0 = (await connection.QueryAsync("select 1 as [Id] where 1 = 0").ConfigureAwait(false)).ToList(); + var data0 = + await connection.QueryAsync("select 1 as [Id] where 1 = 0") + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data0); - var data1 = (await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)).ConfigureAwait(false)).ToList(); + var data1 = + await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)) + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data1); - var data2 = (await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)).ConfigureAwait(false)).ToList(); + var data2 = + await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)) + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data2); - data0 = (await connection.QueryAsync("select 1 as [Id] where 1 = 0").ConfigureAwait(false)).ToList(); + data0 = + await connection.QueryAsync("select 1 as [Id] where 1 = 0") + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data0); - data1 = (await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)).ConfigureAwait(false)).ToList(); + data1 = + await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)) + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data1); - data2 = (await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)).ConfigureAwait(false)).ToList(); + data2 = + await connection.QueryAsync(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)) + .ToListAsync() + .ConfigureAwait(false); Assert.Empty(data2); } @@ -799,24 +861,32 @@ public async Task Issue157_ClosedReaderAsync() { var args = new { x = 42 }; const string sql = "select 123 as [A], 'abc' as [B] where @x=42"; - var row = (await connection.QueryAsync(new CommandDefinition( - sql, args, flags: CommandFlags.None)).ConfigureAwait(false)).Single(); + var row = await connection + .QueryAsync(new CommandDefinition( + sql, args, flags: CommandFlags.None)) + .SingleAsync() + .ConfigureAwait(false); + Assert.NotNull(row); Assert.Equal(123, row.A); Assert.Equal("abc", row.B); args = new { x = 5 }; - Assert.False((await connection.QueryAsync(new CommandDefinition(sql, args, flags: CommandFlags.None)).ConfigureAwait(false)).Any()); + Assert.False(await connection.QueryAsync(new CommandDefinition(sql, args, flags: CommandFlags.None)).AnyAsync().ConfigureAwait(false)); } [Fact] public async Task TestAtEscaping() { - var id = (await connection.QueryAsync(@" - declare @@Name int - select @@Name = @Id+1 - select @@Name - ", new Product { Id = 1 }).ConfigureAwait(false)).Single(); + var id = await connection + .QueryAsync( + @" +declare @@Name int +select @@Name = @Id+1 +select @@Name + ", new Product { Id = 1 }) + .SingleAsync() + .ConfigureAwait(false); Assert.Equal(2, id); } @@ -838,7 +908,10 @@ public async Task Issue563_QueryAsyncShouldThrowException() { try { - var data = (await connection.QueryAsync("select 1 union all select 2; RAISERROR('after select', 16, 1);").ConfigureAwait(false)).ToList(); + var data = + await connection.QueryAsync("select 1 union all select 2; RAISERROR('after select', 16, 1);") + .ToListAsync() + .ConfigureAwait(false); Assert.True(false, "Expected Exception"); } catch (Exception ex) when (ex.GetType().Name == "SqlException" && ex.Message == "after select") { /* swallow only this */ } diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index b9f8b9347..84524adfe 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs index 4d5dd26c4..4fab07e03 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -220,7 +220,7 @@ public async Task TestConversionExceptionMessages() // List paths var list = connection.Query(sql); Assert.Null(Assert.Single(list)); - list = await connection.QueryAsync(sql); + list = await connection.QueryAsync(sql).ToListAsync(); Assert.Null(Assert.Single(list)); // Single row paths @@ -247,7 +247,7 @@ static async Task TestExceptionsAsync(DbConnection connection, string sql, st ex = Assert.Throws(() => connection.QuerySingleOrDefault(sql)); Assert.Equal(exception, ex.Message); - ex = await Assert.ThrowsAsync(() => connection.QueryAsync(sql)); + ex = await Assert.ThrowsAsync(async () => await connection.QueryAsync(sql).ToListAsync()); Assert.Equal(exception, ex.Message); ex = await Assert.ThrowsAsync(() => connection.QueryFirstAsync(sql)); Assert.Equal(exception, ex.Message); @@ -1237,8 +1237,10 @@ public async void SO35470588_WrongValuePidValue() insert TPTable (Value) values (2), (568)"); // fetch the data using the query in the question, then force to a dictionary - var rows = (await connection.QueryAsync("select * from TPTable").ConfigureAwait(false)) - .ToDictionary(x => x.Pid); + var rows = + await connection.QueryAsync("select * from TPTable") + .ToDictionaryAsync(x => x.Pid) + .ConfigureAwait(false); // check the number of rows Assert.Equal(2, rows.Count); diff --git a/tests/Dapper.Tests/ProcedureTests.cs b/tests/Dapper.Tests/ProcedureTests.cs index 2e320779a..7e5cd906e 100644 --- a/tests/Dapper.Tests/ProcedureTests.cs +++ b/tests/Dapper.Tests/ProcedureTests.cs @@ -277,7 +277,8 @@ select 1 as Num end end - exec {tempSPName}"); + exec {tempSPName}") + .ToListAsync(); Assert.Empty(result); } diff --git a/tests/Dapper.Tests/Providers/SqliteTests.cs b/tests/Dapper.Tests/Providers/SqliteTests.cs index a5aeb086f..87594dbb4 100644 --- a/tests/Dapper.Tests/Providers/SqliteTests.cs +++ b/tests/Dapper.Tests/Providers/SqliteTests.cs @@ -74,9 +74,15 @@ public async Task Issue466_SqliteHatesOptimizations_Async() using (var connection = GetSQLiteConnection()) { SqlMapper.ResetTypeHandlers(); - var row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); + var row = + await connection.QueryAsync("select 42 as Id") + .FirstAsync() + .ConfigureAwait(false); Assert.Equal(42, row.Id); - row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); + row = + await connection.QueryAsync("select 42 as Id") + .FirstAsync() + .ConfigureAwait(false); Assert.Equal(42, row.Id); SqlMapper.ResetTypeHandlers(); From 1a2f11d172f2480ccb7775d84f1d58d1e51ff297 Mon Sep 17 00:00:00 2001 From: Vyacheslav Date: Tue, 7 Feb 2023 00:17:32 +0300 Subject: [PATCH 2/4] QueryAsync added parameter "buffered" #1239 --- Dapper/SqlMapper.Async.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index 080728ea7..7bcba8e36 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -19,11 +19,12 @@ public static partial class SqlMapper /// The SQL to execute for the query. /// The parameters to pass, if any. /// The transaction to use, if any. + /// Whether to buffer the results in memory. /// The command timeout (in seconds). /// The type of command to execute. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int ? commandTimeout = null, CommandType? commandType = null) => + QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default)); /// /// Execute a query asynchronously using Task. @@ -195,13 +196,14 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, st /// The SQL to execute for the query. /// The parameters to pass, if any. /// The transaction to use, if any. + /// Whether to buffer the results in memory. /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static IAsyncEnumerable QueryAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int ? commandTimeout = null, CommandType? commandType = null) { if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryAsync(cnn, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); + return QueryAsync(cnn, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default)); } /// From ccbf45b5709fbff3f38c847625ce064905b920b6 Mon Sep 17 00:00:00 2001 From: Vyacheslav Date: Wed, 8 Feb 2023 20:46:36 +0300 Subject: [PATCH 3/4] GridReader have been rewritten from pseudo-asynchronous to real asynchronous. #1239 --- Dapper/SqlMapper.GridReader.Async.cs | 80 ++++++++++++++++++---------- tests/Dapper.Tests/AsyncTests.cs | 32 ++++++----- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/Dapper/SqlMapper.GridReader.Async.cs b/Dapper/SqlMapper.GridReader.Async.cs index bec4967be..5d21d16fb 100644 --- a/Dapper/SqlMapper.GridReader.Async.cs +++ b/Dapper/SqlMapper.GridReader.Async.cs @@ -25,7 +25,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, D /// /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> /// Whether to buffer the results. - public Task> ReadAsync(bool buffered = true) => ReadAsyncImpl(typeof(DapperRow), buffered); + public IAsyncEnumerable ReadAsync(bool buffered = true) => ReadAsyncImpl(typeof(DapperRow), buffered); /// /// Read an individual row of the next grid of results, returned as a dynamic object @@ -57,7 +57,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, D /// The type to read. /// Whether to buffer the results. /// is null. - public Task> ReadAsync(Type type, bool buffered = true) + public IAsyncEnumerable ReadAsync(Type type, bool buffered = true) { if (type == null) throw new ArgumentNullException(nameof(type)); return ReadAsyncImpl(type, buffered); @@ -112,7 +112,7 @@ public Task ReadSingleOrDefaultAsync(Type type) /// /// The type to read. /// Whether the results should be buffered in memory. - public Task> ReadAsync(bool buffered = true) => ReadAsyncImpl(typeof(T), buffered); + public IAsyncEnumerable ReadAsync(bool buffered = true) => ReadAsyncImpl(typeof(T), buffered); /// /// Read an individual row of the next grid of results. @@ -157,7 +157,7 @@ private async Task NextResultAsync() } } - private Task> ReadAsyncImpl(Type type, bool buffered) + private async IAsyncEnumerable ReadAsyncImpl(Type type, bool buffered) { if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); @@ -172,15 +172,58 @@ private Task> ReadAsyncImpl(Type type, bool buffered) cache.Deserializer = deserializer; } IsConsumed = true; - if (buffered && reader is DbDataReader) + if(reader is DbDataReader dbDataReader) { - return ReadBufferedAsync(gridIndex, deserializer.Func); + var index = gridIndex; + if (buffered) + { + var buffer = new List(); + try + { + while (index == gridIndex && await dbDataReader.ReadAsync(cancel).ConfigureAwait(false)) + { + buffer.Add(ConvertTo(deserializer.Func(dbDataReader))); + } + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + await NextResultAsync().ConfigureAwait(false); + } + } + + foreach (var item in buffer) + { + yield return item; + } + } + else + { + try + { + while (index == gridIndex && await dbDataReader.ReadAsync(cancel).ConfigureAwait(false)) + { + yield return ConvertTo(deserializer.Func(dbDataReader)); + } + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + await NextResultAsync().ConfigureAwait(false); + } + } + } } else { var result = ReadDeferred(gridIndex, deserializer.Func, type); - if (buffered) result = result?.ToList(); // for the "not a DbDataReader" scenario - return Task.FromResult(result); + if (buffered) result = result?.ToList(); + foreach (var item in result) + { + yield return item; + } } } @@ -224,27 +267,6 @@ private async Task ReadRowAsyncImplViaDbReader(DbDataReader reader, Type t await NextResultAsync().ConfigureAwait(false); return result; } - - private async Task> ReadBufferedAsync(int index, Func deserializer) - { - try - { - var reader = (DbDataReader)this.reader; - var buffer = new List(); - while (index == gridIndex && await reader.ReadAsync(cancel).ConfigureAwait(false)) - { - buffer.Add(ConvertTo(deserializer(reader))); - } - return buffer; - } - finally // finally so that First etc progresses things even when multiple rows - { - if (index == gridIndex) - { - await NextResultAsync().ConfigureAwait(false); - } - } - } } } } diff --git a/tests/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs index 5ca99517f..59a365523 100644 --- a/tests/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -246,8 +246,10 @@ public async Task TestMultiAsync() { using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false)) { - Assert.Equal(1, multi.ReadAsync().Result.Single()); - Assert.Equal(2, multi.ReadAsync().Result.Single()); + var first = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(1, first); + var second = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(2, second); } } @@ -256,8 +258,10 @@ public async Task TestMultiConversionAsync() { using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select Cast(1 as BigInt) Col1; select Cast(2 as BigInt) Col2").ConfigureAwait(false)) { - Assert.Equal(1, multi.ReadAsync().Result.Single()); - Assert.Equal(2, multi.ReadAsync().Result.Single()); + var first = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(1, first); + var second = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(2, second); } } @@ -267,9 +271,11 @@ public async Task TestMultiAsyncViaFirstOrDefault() using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5").ConfigureAwait(false)) { Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(2, multi.ReadAsync().Result.Single()); + var second = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(2, second); Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(4, multi.ReadAsync().Result.Single()); + var fourth = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(4, fourth); Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); } } @@ -281,8 +287,8 @@ public async Task TestMultiClosedConnAsync() { using (SqlMapper.GridReader multi = await conn.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false)) { - Assert.Equal(1, multi.ReadAsync().Result.Single()); - Assert.Equal(2, multi.ReadAsync().Result.Single()); + Assert.Equal(1, await multi.ReadAsync().SingleAsync().ConfigureAwait(false)); + Assert.Equal(2, await multi.ReadAsync().SingleAsync().ConfigureAwait(false)); } } } @@ -295,9 +301,11 @@ public async Task TestMultiClosedConnAsyncViaFirstOrDefault() using (SqlMapper.GridReader multi = await conn.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5").ConfigureAwait(false)) { Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(2, multi.ReadAsync().Result.Single()); + var second = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(2, second); Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(4, multi.ReadAsync().Result.Single()); + var fourth = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + Assert.Equal(4, fourth); Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); } } @@ -691,8 +699,8 @@ select 42 select 17 SET @AddressPersonId = @PersonId", p).ConfigureAwait(false)) { - x = multi.ReadAsync().Result.Single(); - y = multi.ReadAsync().Result.Single(); + x = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); + y = await multi.ReadAsync().SingleAsync().ConfigureAwait(false); } Assert.Equal("grillmaster", bob.Occupation); From 54556283585abfb6416ee8814ea4b08a584ccf53 Mon Sep 17 00:00:00 2001 From: Vyacheslav Date: Sat, 18 Feb 2023 05:43:20 +0300 Subject: [PATCH 4/4] Microsoft.Bcl.AsyncInterfaces does not support .net 461 --- .../Dapper.EntityFramework.StrongName.csproj | 2 +- Dapper.EntityFramework/Dapper.EntityFramework.csproj | 2 +- Dapper.SqlBuilder/Dapper.SqlBuilder.csproj | 6 +++--- Dapper.StrongName/Dapper.StrongName.csproj | 4 ++-- Dapper/Dapper.csproj | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj b/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj index 4207cd180..87e09a1bb 100644 --- a/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj +++ b/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj @@ -4,7 +4,7 @@ Dapper: Entity Framework type handlers (with a strong name) Extension handlers for entity framework Marc Gravell;Nick Craver - net461 + net462 ../Dapper.snk true true diff --git a/Dapper.EntityFramework/Dapper.EntityFramework.csproj b/Dapper.EntityFramework/Dapper.EntityFramework.csproj index 13310a116..598dcc66c 100644 --- a/Dapper.EntityFramework/Dapper.EntityFramework.csproj +++ b/Dapper.EntityFramework/Dapper.EntityFramework.csproj @@ -5,7 +5,7 @@ Dapper entity framework type handlers 1.50.2 Marc Gravell;Nick Craver - net461 + net462 orm;sql;micro-orm diff --git a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj index 74fe251d3..de2def947 100644 --- a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj +++ b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj @@ -5,17 +5,17 @@ Dapper SqlBuilder component The Dapper SqlBuilder component, for building SQL queries dynamically. Sam Saffron, Johan Danforth - net461;netstandard2.0;net5.0 + net462;netstandard2.1;net5.0 false false - + - + \ No newline at end of file diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index e32d04d9b..9f85dcde9 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -5,7 +5,7 @@ Dapper (Strong Named) A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.1;net5.0 + net462;netstandard2.1;net5.0 true true @@ -22,7 +22,7 @@ - + diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index e42e436a2..4968fa011 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -5,7 +5,7 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.1;net5.0 + net462;netstandard2.1;net5.0 @@ -19,7 +19,7 @@ - +