DynamicParameters.AddParameters(IDbCommand): identity-free self-apply (#2225)

* 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).

* 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.
This commit is contained in:
Marc Gravell
2026-08-20 04:47:55 +01:00
committed by GitHub
parent c0b2097129
commit 41d76c783c
3 changed files with 67 additions and 1 deletions
+16
View File
@@ -147,6 +147,22 @@ namespace Dapper
AddParameters(command, identity);
}
/// <summary>
/// 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 <see cref="SqlMapper.Identity"/>
/// </summary>
/// <param name="command">The raw command prior to execution</param>
public void AddParameters(IDbCommand command)
{
if (command is null) throw new ArgumentNullException(nameof(command));
// 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()));
}
/// <summary>
/// If true, the command-text is inspected and only values that are clearly used are included on the connection
/// </summary>
+2 -1
View File
@@ -1,3 +1,4 @@
#nullable enable
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void
static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void
Dapper.DynamicParameters.AddParameters(System.Data.IDbCommand! command) -> void
+49
View File
@@ -928,6 +928,55 @@ namespace Dapper.Tests
Assert.Null(p.Get<int?>("@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<IDbDataParameter>(command.Parameters[0]);
Assert.Equal("a", p.ParameterName);
Assert.Equal(1, p.Value);
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()
{