From 3838f0ece59fed36d9c5aedb0986364b8d82f07b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 17:11:09 +0100 Subject: [PATCH 1/2] DynamicParameters.AddParameters(IDbCommand): identity-free self-apply The full bag protocol (per-parameter DbType/direction/size, templates, literal replacement, RemoveUnused) already lives in AddParameters(command, identity), but the identity requirement makes it uncallable from outside: Identity's constructor is internal, and identity.Sql is consumed on the first line. This overload builds the identity from the command's own text and type, which is what identity.Sql is in practice - letting external tooling (for example build-time code generators, aka Dapper.AOT) apply a bag without reimplementing the protocol. Test covers the bag+literal composition against a closed connection (no database needed). --- Dapper/DynamicParameters.cs | 14 ++++++++++++++ Dapper/PublicAPI.Unshipped.txt | 3 ++- tests/Dapper.Tests/ParameterTests.cs | 23 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index 1feb4d020..a6fc8933e 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -147,6 +147,20 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id AddParameters(command, identity); } + /// + /// Add all the parameters needed to the command just before it executes, using the + /// command's own text and type for any query-specific handling (literal tokens, etc); + /// this allows external tooling (for example build-time code generators) to apply a + /// parameter bag without constructing an + /// + /// The raw command prior to execution + public void AddParameters(IDbCommand command) + { + if (command is null) throw new ArgumentNullException(nameof(command)); + AddParameters(command, new SqlMapper.Identity(command.CommandText, command.CommandType, + command.Connection!, null, GetType())); + } + /// /// If true, the command-text is inspected and only values that are clearly used are included on the connection /// diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt index cf9343ccf..a4e055ca2 100644 --- a/Dapper/PublicAPI.Unshipped.txt +++ b/Dapper/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ #nullable enable static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool -static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void \ No newline at end of file +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void +Dapper.DynamicParameters.AddParameters(System.Data.IDbCommand! command) -> void diff --git a/tests/Dapper.Tests/ParameterTests.cs b/tests/Dapper.Tests/ParameterTests.cs index 5eb455c65..5bf0b3ce2 100644 --- a/tests/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -928,6 +928,29 @@ public void TestDynamicParamNullSupport() Assert.Null(p.Get("@b")); } + [Fact] + public void AddParameters_Command_AppliesBagAndLiterals() + { + // the identity-free overload: external tooling (e.g. build-time code generators) + // can apply a bag using the command's own text for query-specific handling + using var connection = GetClosedConnection(); + using var command = connection.CreateCommand(); + command.CommandText = "select @a + {=b}"; + + var args = new DynamicParameters(); + args.Add("a", 1); + args.Add("b", 2); + args.AddParameters(command); + + Assert.Equal("select @a + 2", command.CommandText); // literal replaced + // (note the literal member is still attached as a parameter too - unused, but + // that matches what the bag does under vanilla execution) + var p = Assert.IsAssignableFrom(command.Parameters[0]); + Assert.Equal("a", p.ParameterName); + Assert.Equal(1, p.Value); + Assert.Equal(2, command.Parameters.Count); + } + [Fact] public void TestAppendingAnonClasses() { From 79b2960a2fc51d862d320ad9d1d6f3a6d156bfad Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 19:54:31 +0100 Subject: [PATCH 2/2] AddParameters(IDbCommand): dispatch via IDynamicParameters A subclass that hides AddParameters and re-implements the interface - the DynamicParameterWithIntTVP pattern from this very test suite - was skipped by the direct call, which binds statically to the protected base method. Routing through the interface uses the runtime type's interface map, so the subclass version runs, matching what vanilla execution does. --- Dapper/DynamicParameters.cs | 6 ++++-- tests/Dapper.Tests/ParameterTests.cs | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index a6fc8933e..34f0848d9 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -157,8 +157,10 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id public void AddParameters(IDbCommand command) { if (command is null) throw new ArgumentNullException(nameof(command)); - AddParameters(command, new SqlMapper.Identity(command.CommandText, command.CommandType, - command.Connection!, null, GetType())); + // dispatch via the interface so that a subclass which hides AddParameters and + // re-implements IDynamicParameters (an established extension pattern) still runs + ((SqlMapper.IDynamicParameters)this).AddParameters(command, new SqlMapper.Identity( + command.CommandText, command.CommandType, command.Connection!, null, GetType())); } /// diff --git a/tests/Dapper.Tests/ParameterTests.cs b/tests/Dapper.Tests/ParameterTests.cs index 5bf0b3ce2..cdad756ef 100644 --- a/tests/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -951,6 +951,32 @@ public void AddParameters_Command_AppliesBagAndLiterals() Assert.Equal(2, command.Parameters.Count); } + [Fact] + public void AddParameters_Command_DispatchesViaInterface() + { + // a subclass that hides AddParameters and re-implements IDynamicParameters + // (the DynamicParameterWithIntTVP pattern) must still get its version called + using var connection = GetClosedConnection(); + using var command = connection.CreateCommand(); + command.CommandText = "select @a"; + + var args = new HidingBag(); + args.Add("a", 1); + args.AddParameters(command); + + Assert.True(args.CustomRan); + } + + private class HidingBag : DynamicParameters, SqlMapper.IDynamicParameters + { + public bool CustomRan { get; private set; } + public new void AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + base.AddParameters(command, identity); + CustomRan = true; + } + } + [Fact] public void TestAppendingAnonClasses() {