DapperLib/Dapper · error · NotSupportedException
ValueTuple should not be used for parameters - the language-
Error message
ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing
What it means
Thrown by CreateParamInfoGenerator when the type of the `param` object is a ValueTuple (e.g. `(int Id, string Name)`). Dapper reads parameter names via reflection, but ValueTuple field names (Item1/Item2 or the language names) are not stored as real metadata at runtime, so it cannot bind them reliably; it also adds boxing overhead. The check IsValueTuple (SqlMapper.cs:2557) detects any `System.ValueTuple<...>` (including Nullable wrappers) and rejects it.
Source
Thrown at Dapper/SqlMapper.cs:2562
/// Internal use only.
/// </summary>
/// <param name="identity">The identity of the generator.</param>
/// <param name="checkForDuplicates">Whether to check for duplicates.</param>
/// <param name="removeUnused">Whether to remove unused parameters.</param>
public static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) =>
CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.Sql));
private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
&& type.FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true)
|| (type is not null && IsValueTuple(Nullable.GetUnderlyingType(type)));
internal static Action<IDbCommand, object?> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals)
{
Type type = identity.ParametersType!;
if (IsValueTuple(type))
{
throw new NotSupportedException("ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing");
}
bool filterParams = removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text;
if (filterParams && Settings.SupportLegacyParameterTokens)
{
filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql);
}
var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true);
var il = dm.GetILGenerator();
bool isStruct = type.IsValueType;
var _sizeLocal = (LocalBuilder?)null;
LocalBuilder GetSizeLocal() => _sizeLocal ??= il.DeclareLocal(typeof(int));
il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param]
View on GitHub (pinned to 72a54c475f)
Solutions
- Use an anonymous type instead: `new { Id = 1, Name = "x" }` — anonymous-type member names ARE available to Dapper.
- Use a small named class or record for the parameters.
- Use DynamicParameters to add parameters explicitly by name.
- If you received a tuple, project it into an anonymous object before passing to Dapper.
Example fix
// before
var rows = cnn.Query<T>("select * from t where id = @Id", (Id: 1));
// after
var rows = cnn.Query<T>("select * from t where id = @Id", new { Id = 1 }); Defensive patterns
Strategy: validation
Validate before calling
// Convert tuples to anonymous objects before passing to Dapper.
var p = new { Id = tuple.Id, Name = tuple.Name };
var rows = cnn.Query<T>(sql, p); Type guard
static bool IsValueTupleParam(object? p) => p?.GetType().FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true; Prevention
- Use anonymous types or named classes for parameters.
- Never pass a ValueTuple as the param object.
- Project tuples into anonymous objects at the boundary.
When it happens
Trigger: Passing a ValueTuple as the parameter object to any Dapper method: `cnn.Query<T>(sql, (Id: 1, Name: "x"))`. Even though C# lets you name the fields, those names are erased at runtime, so Dapper cannot map them to `@Id`/`@Name`.
Common situations: Using tuple syntax for ad-hoc parameters; returning a tuple from a helper and feeding it straight to Dapper; migrating from anonymous types to tuples for brevity.
Related errors
- The member {name} of type {type.FullName} cannot be used as
- An enumerable sequence of parameters (arrays, lists, etc) is
- When passing parameters by position, each parameter can only
- The first item in a list-expansion cannot be null
- MultiExec is not supported by ExecuteReader
AI-assisted analysis of DapperLib/Dapper@72a54c475f (2026-08-13).
Data as JSON: /api/errors/8d0f43af9158e15c.
Report an issue: GitHub.