Skip to content

Commit b4f80b6

Browse files
authored
Improve performance of "queryunbuffered", and correctness of "first" APIs (#2121)
* see #2115 - correctness: do no use SingleRow by default; affects trailing errors - performance: QueryUnbufferedAsync can mirror cmd?.Cancel() in finally (this is consistent with all other scenarios) * remove settings tweak
1 parent e0479ba commit b4f80b6

File tree

6 files changed

+208
-54
lines changed

6 files changed

+208
-54
lines changed

Diff for: Dapper/SqlMapper.Async.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,11 @@ static async IAsyncEnumerable<T> Impl(IDbConnection cnn, Type effectiveType, Com
13331333
{
13341334
if (reader is not null)
13351335
{
1336+
if (!reader.IsClosed)
1337+
{
1338+
try { cmd?.Cancel(); }
1339+
catch { /* don't spoil any existing exception */ }
1340+
}
13361341
await reader.DisposeAsync();
13371342
}
13381343
if (wasClosed) cnn.Close();

Diff for: Dapper/SqlMapper.Settings.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ public static partial class SqlMapper
1111
/// </summary>
1212
public static class Settings
1313
{
14-
// disable single result by default; prevents errors AFTER the select being detected properly
15-
private const CommandBehavior DefaultAllowedCommandBehaviors = ~CommandBehavior.SingleResult;
14+
// disable single row/result by default; prevents errors AFTER the select being detected properly
15+
private const CommandBehavior DefaultAllowedCommandBehaviors = ~(CommandBehavior.SingleResult | CommandBehavior.SingleRow);
1616
internal static CommandBehavior AllowedCommandBehaviors { get; private set; } = DefaultAllowedCommandBehaviors;
17+
1718
private static void SetAllowedCommandBehaviors(CommandBehavior behavior, bool enabled)
1819
{
1920
if (enabled) AllowedCommandBehaviors |= behavior;

Diff for: Dapper/SqlMapper.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,7 @@ private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandD
11481148
if (!reader.IsClosed)
11491149
{
11501150
try { cmd?.Cancel(); }
1151-
catch { /* don't spoil the existing exception */ }
1151+
catch { /* don't spoil any existing exception */ }
11521152
}
11531153
reader.Dispose();
11541154
}
@@ -1229,7 +1229,7 @@ private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefini
12291229
if (!reader.IsClosed)
12301230
{
12311231
try { cmd?.Cancel(); }
1232-
catch { /* don't spoil the existing exception */ }
1232+
catch { /* don't spoil any existing exception */ }
12331233
}
12341234
reader.Dispose();
12351235
}
@@ -1321,7 +1321,7 @@ private static T QueryRowImpl<T>(IDbConnection cnn, Row row, ref CommandDefiniti
13211321
if (!reader.IsClosed)
13221322
{
13231323
try { cmd?.Cancel(); }
1324-
catch { /* don't spoil the existing exception */ }
1324+
catch { /* don't spoil any existing exception */ }
13251325
}
13261326
reader.Dispose();
13271327
}

Diff for: Directory.Packages.props

+49-49
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,51 @@
11
<Project>
2-
<ItemGroup>
3-
<!-- note: 6.2.0 has regressions; don't force the update -->
4-
<PackageVersion Include="EntityFramework" Version="6.1.3" />
5-
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
6-
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
7-
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
8-
<PackageVersion Include="Microsoft.SqlServer.Types" Version="14.0.1016.290" />
9-
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.143" />
10-
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
11-
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
12-
13-
<!-- tests -->
14-
<PackageVersion Include="Azure.Identity" Version="1.12.1" />
15-
<PackageVersion Include="Belgrade.Sql.Client" Version="1.1.4" />
16-
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
17-
<PackageVersion Include="Dashing" Version="2.10.1" />
18-
<PackageVersion Include="Dapper.Contrib" Version="2.0.78" />
19-
<PackageVersion Include="DuckDB.NET.Data.Full" Version="1.1.1" />
20-
<PackageVersion Include="DevExpress.Xpo" Version="24.1.6" />
21-
<PackageVersion Include="FirebirdSql.Data.FirebirdClient" Version="10.3.1" />
22-
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
23-
<PackageVersion Include="Iesi.Collections" Version="4.1.1" />
24-
<PackageVersion Include="linq2db.SqlServer" Version="5.4.1" />
25-
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
26-
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
27-
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
28-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
29-
<PackageVersion Include="Mighty" Version="3.2.0" />
30-
<PackageVersion Include="MySqlConnector" Version="2.3.7" />
31-
<PackageVersion Include="NHibernate" Version="5.5.2" />
32-
<PackageVersion Include="Norm.net" Version="5.4.0" />
33-
<PackageVersion Include="Npgsql" Version="8.0.4" />
34-
<PackageVersion Include="PetaPoco" Version="5.1.306" />
35-
<PackageVersion Include="RepoDb.SqlServer" Version="1.13.1" />
36-
<PackageVersion Include="ServiceStack.OrmLite.SqlServer" Version="8.4.0" />
37-
<PackageVersion Include="Snowflake.Data" Version="4.1.0" />
38-
<PackageVersion Include="SqlMarshal" Version="0.5.0" />
39-
<PackageVersion Include="SubSonic" Version="3.0.0.4" />
40-
<PackageVersion Include="Susanoo.SqlServer" Version="1.2.4.2" />
41-
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
42-
<PackageVersion Include="System.Data.SQLite" Version="1.0.119" />
43-
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
44-
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
45-
<PackageVersion Include="System.Reflection.Metadata" Version="8.0.0" />
46-
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
47-
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
48-
<PackageVersion Include="xunit" Version="2.9.2" />
49-
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
50-
</ItemGroup>
2+
<ItemGroup>
3+
<!-- note: 6.2.0 has regressions; don't force the update -->
4+
<PackageVersion Include="EntityFramework" Version="6.1.3" />
5+
<PackageVersion Include="FastMember" Version="1.5.0" />
6+
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
7+
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
8+
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
9+
<PackageVersion Include="Microsoft.SqlServer.Types" Version="14.0.1016.290" />
10+
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.143" />
11+
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
12+
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
13+
<!-- tests -->
14+
<PackageVersion Include="Azure.Identity" Version="1.12.1" />
15+
<PackageVersion Include="Belgrade.Sql.Client" Version="1.1.4" />
16+
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
17+
<PackageVersion Include="Dashing" Version="2.10.1" />
18+
<PackageVersion Include="Dapper.Contrib" Version="2.0.78" />
19+
<PackageVersion Include="DuckDB.NET.Data.Full" Version="1.1.1" />
20+
<PackageVersion Include="DevExpress.Xpo" Version="24.1.6" />
21+
<PackageVersion Include="FirebirdSql.Data.FirebirdClient" Version="10.3.1" />
22+
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
23+
<PackageVersion Include="Iesi.Collections" Version="4.1.1" />
24+
<PackageVersion Include="linq2db.SqlServer" Version="5.4.1" />
25+
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
26+
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
27+
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
28+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
29+
<PackageVersion Include="Mighty" Version="3.2.0" />
30+
<PackageVersion Include="MySqlConnector" Version="2.3.7" />
31+
<PackageVersion Include="NHibernate" Version="5.5.2" />
32+
<PackageVersion Include="Norm.net" Version="5.4.0" />
33+
<PackageVersion Include="Npgsql" Version="8.0.4" />
34+
<PackageVersion Include="PetaPoco" Version="5.1.306" />
35+
<PackageVersion Include="RepoDb.SqlServer" Version="1.13.1" />
36+
<PackageVersion Include="ServiceStack.OrmLite.SqlServer" Version="8.4.0" />
37+
<PackageVersion Include="Snowflake.Data" Version="4.1.0" />
38+
<PackageVersion Include="SqlMarshal" Version="0.5.0" />
39+
<PackageVersion Include="SubSonic" Version="3.0.0.4" />
40+
<PackageVersion Include="Susanoo.SqlServer" Version="1.2.4.2" />
41+
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
42+
<PackageVersion Include="System.Data.SQLite" Version="1.0.119" />
43+
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
44+
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
45+
<PackageVersion Include="System.Reflection.Metadata" Version="8.0.0" />
46+
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
47+
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
48+
<PackageVersion Include="xunit" Version="2.9.2" />
49+
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
50+
</ItemGroup>
5151
</Project>

Diff for: tests/Dapper.Tests/Dapper.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<DefineConstants>$(DefineConstants);MSSQLCLIENT</DefineConstants>
77
<NoWarn>$(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208;CA1861</NoWarn>
88
<Nullable>enable</Nullable>
9+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
910
</PropertyGroup>
1011

1112
<PropertyGroup Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">
@@ -16,6 +17,7 @@
1617
<ProjectReference Include="../../Dapper.ProviderTools/Dapper.ProviderTools.csproj" />
1718
<ProjectReference Include="../../Dapper.SqlBuilder/Dapper.SqlBuilder.csproj" />
1819
<PackageReference Include="DuckDB.NET.Data.Full" />
20+
<PackageReference Include="FastMember" />
1921
<PackageReference Include="FirebirdSql.Data.FirebirdClient" />
2022
<PackageReference Include="Microsoft.Data.SqlClient" />
2123
<PackageReference Include="Microsoft.Data.Sqlite" />

Diff for: tests/Dapper.Tests/SingleRowTests.cs

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data.Common;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using FastMember;
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
using static Dapper.SqlMapper;
13+
14+
namespace Dapper.Tests;
15+
16+
[Collection("SingleRowTests")]
17+
public sealed class SystemSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests<SystemSqlClientProvider>(log)
18+
{
19+
protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source)
20+
{
21+
using var bcp = new System.Data.SqlClient.SqlBulkCopy((System.Data.SqlClient.SqlConnection)conn);
22+
bcp.DestinationTableName = "#mydata";
23+
bcp.EnableStreaming = true;
24+
await bcp.WriteToServerAsync(source);
25+
}
26+
}
27+
#if MSSQLCLIENT
28+
[Collection("SingleRowTests")]
29+
public sealed class MicrosoftSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests<MicrosoftSqlClientProvider>(log)
30+
{
31+
protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source)
32+
{
33+
using var bcp = new Microsoft.Data.SqlClient.SqlBulkCopy((Microsoft.Data.SqlClient.SqlConnection)conn);
34+
bcp.DestinationTableName = "#mydata";
35+
bcp.EnableStreaming = true;
36+
await bcp.WriteToServerAsync(source);
37+
}
38+
}
39+
#endif
40+
public abstract class SingleRowTests<TProvider>(ITestOutputHelper log) : TestBase<TProvider> where TProvider : DatabaseProvider
41+
{
42+
protected abstract Task InjectDataAsync(DbConnection connection, DbDataReader source);
43+
44+
[Fact]
45+
public async Task QueryFirst_PerformanceAndCorrectness()
46+
{
47+
using var conn = GetOpenConnection();
48+
conn.Execute("create table #mydata(id int not null, name nvarchar(250) not null)");
49+
50+
var rand = new Random();
51+
var data = from id in Enumerable.Range(1, 500_000)
52+
select new MyRow { Id = rand.Next(), Name = CreateName(rand) };
53+
54+
Stopwatch watch;
55+
using (var reader = ObjectReader.Create(data))
56+
{
57+
await InjectDataAsync(conn, reader);
58+
watch = Stopwatch.StartNew();
59+
var count = await conn.QuerySingleAsync<int>("""select count(1) from #mydata""");
60+
watch.Stop();
61+
log.WriteLine($"bulk-insert complete; {count} rows in {watch.ElapsedMilliseconds}ms");
62+
}
63+
64+
// just errors
65+
var ex = Assert.ThrowsAny<DbException>(() => conn.Execute("raiserror('bad things', 16, 1)"));
66+
log.WriteLine(ex.Message);
67+
ex = await Assert.ThrowsAnyAsync<DbException>(async () => await conn.ExecuteAsync("raiserror('bad things', 16, 1)"));
68+
log.WriteLine(ex.Message);
69+
70+
// just data
71+
watch = Stopwatch.StartNew();
72+
var row = conn.QueryFirst<MyRow>("select top 1 * from #mydata");
73+
watch.Stop();
74+
log.WriteLine($"sync top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
75+
76+
watch = Stopwatch.StartNew();
77+
row = await conn.QueryFirstAsync<MyRow>("select top 1 * from #mydata");
78+
watch.Stop();
79+
log.WriteLine($"async top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
80+
81+
watch = Stopwatch.StartNew();
82+
row = conn.QueryFirst<MyRow>("select * from #mydata");
83+
watch.Stop();
84+
log.WriteLine($"sync read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
85+
86+
watch = Stopwatch.StartNew();
87+
row = await conn.QueryFirstAsync<MyRow>("select * from #mydata");
88+
watch.Stop();
89+
log.WriteLine($"async read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
90+
91+
// data with trailing errors
92+
93+
watch = Stopwatch.StartNew();
94+
ex = Assert.ThrowsAny<DbException>(() => conn.QueryFirst<MyRow>("select * from #mydata; raiserror('bad things', 16, 1)"));
95+
watch.Stop();
96+
log.WriteLine($"sync read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}");
97+
98+
watch = Stopwatch.StartNew();
99+
ex = await Assert.ThrowsAnyAsync<DbException>(async () => await conn.QueryFirstAsync<MyRow>("select * from #mydata; raiserror('bad things', 16, 1)"));
100+
watch.Stop();
101+
log.WriteLine($"async read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}");
102+
103+
// unbuffered read with trailing errors - do not expect to see this unless we consume all!
104+
105+
watch = Stopwatch.StartNew();
106+
row = conn.Query<MyRow>("select * from #mydata", buffered: false).First();
107+
watch.Stop();
108+
log.WriteLine($"sync unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
109+
110+
#if NET5_0_OR_GREATER
111+
watch = Stopwatch.StartNew();
112+
row = await conn.QueryUnbufferedAsync<MyRow>("select * from #mydata").FirstAsync();
113+
watch.Stop();
114+
log.WriteLine($"async unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
115+
#endif
116+
117+
static unsafe string CreateName(Random rand)
118+
{
119+
const string Alphabet = "abcdefghijklmnopqrstuvwxyz 0123456789,;-";
120+
var len = rand.Next(5, 251);
121+
char* ptr = stackalloc char[len];
122+
for (int i = 0; i < len; i++)
123+
{
124+
ptr[i] = Alphabet[rand.Next(Alphabet.Length)];
125+
}
126+
return new string(ptr, 0, len);
127+
}
128+
129+
}
130+
131+
public class MyRow
132+
{
133+
public int Id { get; set; }
134+
public string Name { get; set; } = "";
135+
}
136+
}
137+
138+
internal static class AsyncLinqHelper
139+
{
140+
public static async ValueTask<T> FirstAsync<T>(this IAsyncEnumerable<T> source, CancellationToken cancellationToken = default)
141+
{
142+
await using var iter = source.GetAsyncEnumerator(cancellationToken);
143+
if (!await iter.MoveNextAsync()) Array.Empty<T>().First(); // for consistent error
144+
return iter.Current;
145+
}
146+
}

0 commit comments

Comments
 (0)