From 3838f0ece59fed36d9c5aedb0986364b8d82f07b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 17:11:09 +0100 Subject: [PATCH] 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 1feb4d02..a6fc8933 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -147,6 +147,20 @@ namespace Dapper 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 cf9343cc..a4e055ca 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 5eb455c6..5bf0b3ce 100644 --- a/tests/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -928,6 +928,29 @@ namespace Dapper.Tests 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() {