From 24d7a33779697b1fdf1774d0041a1a6a30e84018 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Sat, 24 Oct 2026 07:43:28 -0500 Subject: [PATCH 1/2] Report empty GridReader results as consumed --- Dapper/SqlMapper.GridReader.cs | 14 +++++++++----- Directory.Packages.props | 1 + tests/Dapper.Tests/Dapper.Tests.csproj | 5 +++-- tests/Dapper.Tests/FakeDbTests.Issue751.cs | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 tests/Dapper.Tests/FakeDbTests.Issue751.cs diff --git a/Dapper/SqlMapper.GridReader.cs b/Dapper/SqlMapper.GridReader.cs index 1370f7cc9..88f6773de 100644 --- a/Dapper/SqlMapper.GridReader.cs +++ b/Dapper/SqlMapper.GridReader.cs @@ -33,11 +33,15 @@ protected GridReader(IDbCommand command, DbDataReader reader, Identity? identity Command = command; this.reader = reader; _identity = identity; - this.onCompleted = onCompleted; - this.state = state; - this.addToCache = addToCache; - cancel = cancellationToken; - } + this.onCompleted = onCompleted; + this.state = state; + this.addToCache = addToCache; + cancel = cancellationToken; + if (reader.FieldCount == 0) + { + _resultIndexAndConsumedFlag = CONSUMED_FLAG; + } + } internal GridReader(IDbCommand command, DbDataReader reader, Identity identity, IParameterCallbacks? callbacks, bool addToCache, CancellationToken cancellationToken = default) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f91a3cca..8f2772419 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,6 +35,7 @@ + diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj index 93b498618..09947a89a 100644 --- a/tests/Dapper.Tests/Dapper.Tests.csproj +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -31,8 +31,9 @@ - - + + + diff --git a/tests/Dapper.Tests/FakeDbTests.Issue751.cs b/tests/Dapper.Tests/FakeDbTests.Issue751.cs new file mode 100644 index 000000000..b6e06be5f --- /dev/null +++ b/tests/Dapper.Tests/FakeDbTests.Issue751.cs @@ -0,0 +1,21 @@ +#if !NET481 +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public sealed class FakeDbIssue751Tests + { + [Fact] + public void IsConsumedIsTrueWhenReaderHasNoResultSets() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + using var grid = conn.QueryMultiple("exec NoResultSets"); + + Assert.True(grid.IsConsumed); + } + } +} +#endif From 6db1a51684085d2cb8312cd98c1d3f876d83ef5e Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Sat, 24 Oct 2026 10:30:27 -0500 Subject: [PATCH 2/2] Skip empty leading results before reporting GridReader consumed A zero-column result followed by a real result set is not a consumed reader. Skip leading empty results lazily and report consumed only when no further result exists. Adds multi-result-set regression tests. Co-Authored-By: Claude Sonnet 5.5 --- Dapper/SqlMapper.GridReader.cs | 44 +++++++++--- tests/Dapper.Tests/FakeDbTests.Issue751.cs | 82 ++++++++++++++++------ 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/Dapper/SqlMapper.GridReader.cs b/Dapper/SqlMapper.GridReader.cs index 88f6773de..208382ebb 100644 --- a/Dapper/SqlMapper.GridReader.cs +++ b/Dapper/SqlMapper.GridReader.cs @@ -33,15 +33,11 @@ protected GridReader(IDbCommand command, DbDataReader reader, Identity? identity Command = command; this.reader = reader; _identity = identity; - this.onCompleted = onCompleted; - this.state = state; - this.addToCache = addToCache; - cancel = cancellationToken; - if (reader.FieldCount == 0) - { - _resultIndexAndConsumedFlag = CONSUMED_FLAG; - } - } + this.onCompleted = onCompleted; + this.state = state; + this.addToCache = addToCache; + cancel = cancellationToken; + } internal GridReader(IDbCommand command, DbDataReader reader, Identity identity, IParameterCallbacks? callbacks, bool addToCache, CancellationToken cancellationToken = default) @@ -421,7 +417,35 @@ private IEnumerable ReadDeferred(int index, Func des /// Has the underlying reader been consumed? /// /// This also reports true if the current grid is actively being consumed - public bool IsConsumed => (_resultIndexAndConsumedFlag & CONSUMED_FLAG) != 0; + public bool IsConsumed + { + get + { + SkipLeadingEmptyResults(); + return (_resultIndexAndConsumedFlag & CONSUMED_FLAG) != 0; + } + } + + private bool _checkedLeadingResults; + + // A reader positioned on a result without columns (for example a procedure that + // selects in a loop which never iterated) has nothing to read. Skip such results; only + // when no further result exists is the grid consumed. A zero-column result followed by a + // real result set must not be reported as consumed. + private void SkipLeadingEmptyResults() + { + if (_checkedLeadingResults) return; + _checkedLeadingResults = true; + if (reader is null || reader.IsClosed || _resultIndexAndConsumedFlag != 0) return; + while (reader.FieldCount == 0) + { + if (!reader.NextResult()) + { + _resultIndexAndConsumedFlag |= CONSUMED_FLAG; + return; + } + } + } /// /// The command associated with the reader diff --git a/tests/Dapper.Tests/FakeDbTests.Issue751.cs b/tests/Dapper.Tests/FakeDbTests.Issue751.cs index b6e06be5f..898a09f72 100644 --- a/tests/Dapper.Tests/FakeDbTests.Issue751.cs +++ b/tests/Dapper.Tests/FakeDbTests.Issue751.cs @@ -1,21 +1,61 @@ -#if !NET481 -using pengdows.crud.fakeDb; -using Xunit; - -namespace Dapper.Tests -{ - public sealed class FakeDbIssue751Tests - { - [Fact] - public void IsConsumedIsTrueWhenReaderHasNoResultSets() - { - using var conn = new fakeDbConnection(new FakeDataStore()); - conn.Open(); - - using var grid = conn.QueryMultiple("exec NoResultSets"); - - Assert.True(grid.IsConsumed); - } - } -} -#endif +#if !NET481 +using System.Collections.Generic; +using System.Linq; +using pengdows.crud.fakeDb; +using Xunit; + +namespace Dapper.Tests +{ + public sealed class FakeDbIssue751Tests + { + [Fact] + public void IsConsumedIsTrueWhenReaderHasNoResultSets() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.Open(); + + using var grid = conn.QueryMultiple("exec NoResultSets"); + + Assert.True(grid.IsConsumed); + } + + [Fact] + public void IsConsumedIsFalseWhenEmptyResultPrecedesRealResultSet() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueMultiResultReader(new[] + { + Enumerable.Empty>(), + new[] { new Dictionary { ["Value"] = 7 } } + }); + conn.Open(); + + using var grid = conn.QueryMultiple("insert x; select 7"); + + Assert.False(grid.IsConsumed); + Assert.Equal(7, grid.Read().Single()); + Assert.True(grid.IsConsumed); + } + + [Fact] + public void IsConsumedIsFalseForNonEmptyFirstResultAndTrueAfterLast() + { + using var conn = new fakeDbConnection(new FakeDataStore()); + conn.EnqueueMultiResultReader(new[] + { + new[] { new Dictionary { ["Value"] = 1 } }, + new[] { new Dictionary { ["Value"] = 2 } } + }); + conn.Open(); + + using var grid = conn.QueryMultiple("select 1; select 2"); + + Assert.False(grid.IsConsumed); + Assert.Equal(1, grid.Read().Single()); + Assert.False(grid.IsConsumed); + Assert.Equal(2, grid.Read().Single()); + Assert.True(grid.IsConsumed); + } + } +} +#endif