DapperLib/Dapper · error · ObjectDisposedException

The reader has been disposed; this can happen after all data

Error message

The reader has been disposed; this can happen after all data has been consumed

What it means

OnBeforeGrid() is the guard every Read/ReadAsync path calls (SqlMapper.GridReader.cs:183). It throws ObjectDisposedException when the internal reader field is null — Dispose() nulls it. The message notes this can also happen after all data has been consumed, since the reader is torn down at that point.

Source

Thrown at Dapper/SqlMapper.GridReader.cs:185

            /// <summary>
            /// Read an individual row of the next grid of results.
            /// </summary>
            /// <param name="type">The type to read.</param>
            /// <exception cref="ArgumentNullException"><paramref name="type"/> is <c>null</c>.</exception>
            public object? ReadSingleOrDefault(Type type)
            {
                if (type is null) throw new ArgumentNullException(nameof(type));
                return ReadRow<object>(type, Row.SingleOrDefault);
            }


            /// <summary>
            /// Validates that data is available, returning the <see cref="ResultIndex"/> that corresponds to the current grid - and marks the current grid as consumed;
            /// this call <em>must</em> be paired with a call to <see cref="OnAfterGrid(int)"/> or <see cref="OnAfterGridAsync(int)"/>
            /// </summary>
            protected int OnBeforeGrid()
            {
                if (reader is 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");
                _resultIndexAndConsumedFlag |= CONSUMED_FLAG;
                return ResultIndex;
            }

            private IEnumerable<T> ReadImpl<T>(Type type, bool buffered)
            {
                var index = OnBeforeGrid();
                var typedIdentity = Identity.ForGrid(type, index);
                CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache);
                var deserializer = cache.Deserializer;

                int hash = GetColumnHash(reader);
                if (deserializer.Func is null || deserializer.Hash != hash)
                {
                    deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false));
                    cache.Deserializer = deserializer;
                }

View on GitHub (pinned to 72a54c475f)

Solutions

  1. Always wrap the GridReader in using: using var grid = cnn.QueryMultiple(sql); then read every grid inside the scope.
  2. Read exactly the number of result sets the query produces; track count and stop.
  3. Do not Dispose or let the using exit before all reads (sync) / all awaited reads (async) complete.
  4. For async, await every ReadAsync inside the using scope before it disposes.

Example fix

// before
var grid = cnn.QueryMultiple(sql);
var a = grid.Read<A>();
// grid falls out of scope, disposed -> later read throws

// after
using var grid = cnn.QueryMultiple(sql);
var a = grid.Read<A>();
var b = grid.Read<B>(); // both reads inside the scope
Defensive patterns

Strategy: validation

Validate before calling

// Structural guard: read only while the grid is alive
using var grid = cnn.QueryMultiple(sql);
if (grid.IsConsumed) throw new InvalidOperationException("GridReader already consumed");
var rows = grid.Read<Foo>();

Try / catch

try { var rows = grid.Read<Foo>(); }
catch (ObjectDisposedException) { /* grid was disposed; re-run QueryMultiple */ }

Prevention

When it happens

Trigger: Call any Read/ReadFirst/ReadAsync after grid.Dispose(); or after the using block holding the GridReader has exited; or attempt to read more result sets than the query returned (the last grid's consumption disposes the reader).

Common situations: Missing `using` on the GridReader; reading grids asynchronously after the connection/command scope closed; sharing a GridReader across threads where one disposes; off-by-one in grid count when looping.

Related errors


AI-assisted analysis of DapperLib/Dapper@72a54c475f (2026-08-13). Data as JSON: /api/errors/1b452c4483ce461c. Report an issue: GitHub.