DapperLib/Dapper · error · ObjectDisposedException

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

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.

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.

Appendix: 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)