Skip to content

Fixes QueryMultiple corrupting a later Query<T>'s DynamicParameters -- fixes #243, #2091#2214

Open
NadeemAfana wants to merge 1 commit into
DapperLib:mainfrom
NadeemAfana:bug/243_and_2091
Open

Fixes QueryMultiple corrupting a later Query<T>'s DynamicParameters -- fixes #243, #2091#2214
NadeemAfana wants to merge 1 commit into
DapperLib:mainfrom
NadeemAfana:bug/243_and_2091

Conversation

@NadeemAfana

Copy link
Copy Markdown

Summary

Calling a query (stored proc or text) via QueryMultiple(...).Read<T>() and then via Query<T>(...)/QueryAsync<T>(...) with DynamicParameters on the same SQL can cause the second call to send the wrong parameters, typically dropping them entirely. Depending on the target's signature this surfaces as:

It's intermittent in real apps (depends on which call populates the shared cache entry first) and clears on app restart because the corruption lives in the process-wide query cache.

Fixes #243. Fixes #2091.

Root cause

Query<T> and the first grid of a QueryMultiple over the same SQL share a single cache entry. A grid read only needs the deserializer and has no parameter instance, so it populates that shared entry with a null parameter
object. For DynamicParameters (whose real parameters live in an internal collection, not in the type's properties), Dapper then builds a parameter-reader from the type's properties (ParameterNames, RemoveUnused) that never emits the actual parameters. Because the grid identity still carries the parameter type, it collides with the Query<T> identity, so a later Query<T>
with DynamicParameters reuses that wrong reader and sends the wrong parameters (or none). Whichever call populates the entry first wins, which is why it's intermittent and clears on restart.

Anonymous/POCO parameters are unaffected since their parameters are their properties.

Fix

Identity.ForGrid(...) now passes null for parametersType (grid reads never bind parameters).
This:

  1. de-collides grid identities from Query<T> identities, so a Query<T> always builds/uses its own correct ParamReader; and
  2. makes GetCacheInfo skip building a ParamReader for grid reads.

Impact / trade-offs

  • Parameter binding is unaffected for normal queries; the command-level identity still carries parametersType.
  • The grid-0 deserializer for (sql, T) is no longer shared with a Query<T> for the same (sql, T) so at most one extra deserializer compile (one-time, cached). No per-call overhead.
    Conversely, deserializers are now shared across different parameter types, which the old keying prevented (a small net win).
  • A grid read can still share an entry with a parameterless Query<T>(sql) (both have parametersType == null); that's harmless as neither builds a ParamReader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant