From ecb63a3cbf827657dde6825a25d222ee2064ad10 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 19:53:22 +0100 Subject: [PATCH 1/2] Clear parameters when tearing down a command Vanilla Dapper's finally blocks all do cmd.Parameters.Clear() before the command is disposed, and that is load-bearing rather than tidy: a parameter object the caller supplied - an ICustomQueryParameter's DbParameter, or one added to DynamicParameters directly - stays owned by the dead command's collection otherwise, and the next use throws 'The SqlParameter is already contained by another SqlParameterCollection'. UnifiedCommand.Cleanup now does the same; recycled commands keep their parameters, as before, since recycling does not pass through Cleanup. --- src/Dapper.AOT/UnifiedCommand.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Dapper.AOT/UnifiedCommand.cs b/src/Dapper.AOT/UnifiedCommand.cs index f7b69500..6744fbad 100644 --- a/src/Dapper.AOT/UnifiedCommand.cs +++ b/src/Dapper.AOT/UnifiedCommand.cs @@ -219,7 +219,14 @@ private DbCommand UnsafeWithCommandForParameters() internal void Cleanup() { - dbCommand?.Dispose(); + if (dbCommand is not null) + { + // match vanilla Dapper's teardown: a parameter object the caller supplied + // (ICustomQueryParameter, DynamicParameters.Add(DbParameter), etc) must not + // stay owned by a dead command's collection, or it cannot be reused + dbCommand.Parameters.Clear(); + dbCommand.Dispose(); + } #if NET6_0_OR_GREATER batch?.Dispose(); #endif From da3c7da583bad5ebd40554304d76524a0ef5ac5d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 19:57:11 +0100 Subject: [PATCH 2/2] Also clear parameters in the state Dispose paths UnifiedCommand.Cleanup was the wrong (or at least insufficient) place: the query and execute pipelines dispose the command via SyncCommandState / AsyncCommandState, which never pass through Cleanup. Recycled commands are nulled out of the state before Dispose runs, so their parameters are still kept for in-place update, as before. --- src/Dapper.AOT/Internal/AsyncCommandState.cs | 9 ++++++++- src/Dapper.AOT/Internal/SyncCommandState.cs | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Dapper.AOT/Internal/AsyncCommandState.cs b/src/Dapper.AOT/Internal/AsyncCommandState.cs index 4922de8b..ae3e8086 100644 --- a/src/Dapper.AOT/Internal/AsyncCommandState.cs +++ b/src/Dapper.AOT/Internal/AsyncCommandState.cs @@ -123,6 +123,8 @@ public virtual ValueTask DisposeAsync() if (cmd is not null) { + // match vanilla's teardown: release caller-supplied parameter objects + cmd.Parameters.Clear(); if (conn is not null && (_flags & FLAG_CLOSE_CONNECTION) != 0) { // need to close the connection and dispose the command @@ -178,7 +180,12 @@ public virtual void Dispose() { var cmd = Command; Command = null; - cmd?.Dispose(); + if (cmd is not null) + { + // match vanilla's teardown: release caller-supplied parameter objects + cmd.Parameters.Clear(); + cmd.Dispose(); + } var conn = connection; connection = null; diff --git a/src/Dapper.AOT/Internal/SyncCommandState.cs b/src/Dapper.AOT/Internal/SyncCommandState.cs index 90674cbb..5ff39a72 100644 --- a/src/Dapper.AOT/Internal/SyncCommandState.cs +++ b/src/Dapper.AOT/Internal/SyncCommandState.cs @@ -62,7 +62,12 @@ public void Dispose() { var cmd = Command; Command = null; - cmd?.Dispose(); + if (cmd is not null) + { + // match vanilla's teardown: release caller-supplied parameter objects + cmd.Parameters.Clear(); + cmd.Dispose(); + } var conn = connection; connection = null;