Skip to content

Commit da781c4

Browse files
committed
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)
1 parent e0479ba commit da781c4

File tree

6 files changed

+210
-54
lines changed

6 files changed

+210
-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

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
Settings.SetDefaults();
48+
49+
using var conn = GetOpenConnection();
50+
conn.Execute("create table #mydata(id int not null, name nvarchar(250) not null)");
51+
52+
var rand = new Random();
53+
var data = from id in Enumerable.Range(1, 500_000)
54+
select new MyRow { Id = rand.Next(), Name = CreateName(rand) };
55+
56+
Stopwatch watch;
57+
using (var reader = ObjectReader.Create(data))
58+
{
59+
await InjectDataAsync(conn, reader);
60+
watch = Stopwatch.StartNew();
61+
var count = await conn.QuerySingleAsync<int>("""select count(1) from #mydata""");
62+
watch.Stop();
63+
log.WriteLine($"bulk-insert complete; {count} rows in {watch.ElapsedMilliseconds}ms");
64+
}
65+
66+
// just errors
67+
var ex = Assert.ThrowsAny<DbException>(() => conn.Execute("raiserror('bad things', 16, 1)"));
68+
log.WriteLine(ex.Message);
69+
ex = await Assert.ThrowsAnyAsync<DbException>(async () => await conn.ExecuteAsync("raiserror('bad things', 16, 1)"));
70+
log.WriteLine(ex.Message);
71+
72+
// just data
73+
watch = Stopwatch.StartNew();
74+
var row = conn.QueryFirst<MyRow>("select top 1 * from #mydata");
75+
watch.Stop();
76+
log.WriteLine($"sync top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
77+
78+
watch = Stopwatch.StartNew();
79+
row = await conn.QueryFirstAsync<MyRow>("select top 1 * from #mydata");
80+
watch.Stop();
81+
log.WriteLine($"async top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
82+
83+
watch = Stopwatch.StartNew();
84+
row = conn.QueryFirst<MyRow>("select * from #mydata");
85+
watch.Stop();
86+
log.WriteLine($"sync read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
87+
88+
watch = Stopwatch.StartNew();
89+
row = await conn.QueryFirstAsync<MyRow>("select * from #mydata");
90+
watch.Stop();
91+
log.WriteLine($"async read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
92+
93+
// data with trailing errors
94+
95+
watch = Stopwatch.StartNew();
96+
ex = Assert.ThrowsAny<DbException>(() => conn.QueryFirst<MyRow>("select * from #mydata; raiserror('bad things', 16, 1)"));
97+
watch.Stop();
98+
log.WriteLine($"sync read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}");
99+
100+
watch = Stopwatch.StartNew();
101+
ex = await Assert.ThrowsAnyAsync<DbException>(async () => await conn.QueryFirstAsync<MyRow>("select * from #mydata; raiserror('bad things', 16, 1)"));
102+
watch.Stop();
103+
log.WriteLine($"async read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}");
104+
105+
// unbuffered read with trailing errors - do not expect to see this unless we consume all!
106+
107+
watch = Stopwatch.StartNew();
108+
row = conn.Query<MyRow>("select * from #mydata", buffered: false).First();
109+
watch.Stop();
110+
log.WriteLine($"sync unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
111+
112+
#if NET5_0_OR_GREATER
113+
watch = Stopwatch.StartNew();
114+
row = await conn.QueryUnbufferedAsync<MyRow>("select * from #mydata").FirstAsync();
115+
watch.Stop();
116+
log.WriteLine($"async unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms");
117+
#endif
118+
119+
static unsafe string CreateName(Random rand)
120+
{
121+
const string Alphabet = "abcdefghijklmnopqrstuvwxyz 0123456789,;-";
122+
var len = rand.Next(5, 251);
123+
char* ptr = stackalloc char[len];
124+
for (int i = 0; i < len; i++)
125+
{
126+
ptr[i] = Alphabet[rand.Next(Alphabet.Length)];
127+
}
128+
return new string(ptr, 0, len);
129+
}
130+
131+
}
132+
133+
public class MyRow
134+
{
135+
public int Id { get; set; }
136+
public string Name { get; set; } = "";
137+
}
138+
}
139+
140+
internal static class AsyncLinqHelper
141+
{
142+
public static async ValueTask<T> FirstAsync<T>(this IAsyncEnumerable<T> source, CancellationToken cancellationToken = default)
143+
{
144+
await using var iter = source.GetAsyncEnumerator(cancellationToken);
145+
if (!await iter.MoveNextAsync()) Array.Empty<T>().First(); // for consistent error
146+
return iter.Current;
147+
}
148+
}

0 commit comments

Comments
 (0)