{"record":{"id":"8d0f43af9158e15c","repo":"DapperLib/Dapper","slug":"valuetuple-should-not-be-used-for-parameters-the","errorCode":null,"errorMessage":"ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing","messagePattern":"ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"Dapper/SqlMapper.cs","lineNumber":2562,"sourceCode":"        /// Internal use only.\r\n        /// </summary>\r\n        /// <param name=\"identity\">The identity of the generator.</param>\r\n        /// <param name=\"checkForDuplicates\">Whether to check for duplicates.</param>\r\n        /// <param name=\"removeUnused\">Whether to remove unused parameters.</param>\r\n        public static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) =>\r\n            CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.Sql));\r\n\r\n        private static bool IsValueTuple(Type? type) => (type?.IsValueType == true\r\n                                                       && type.FullName?.StartsWith(\"System.ValueTuple`\", StringComparison.Ordinal) == true)\r\n                                                       || (type is not null && IsValueTuple(Nullable.GetUnderlyingType(type)));\r\n\r\n        internal static Action<IDbCommand, object?> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals)\r\n        {\r\n            Type type = identity.ParametersType!;\r\n\r\n            if (IsValueTuple(type))\r\n            {\r\n                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\");\r\n            }\r\n\r\n            bool filterParams = removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text;\r\n            \r\n            if (filterParams && Settings.SupportLegacyParameterTokens)\r\n            {\r\n                filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql);\r\n            }\r\n            \r\n            var dm = new DynamicMethod(\"ParamInfo\" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true);\r\n\r\n            var il = dm.GetILGenerator();\r\n\r\n            bool isStruct = type.IsValueType;\r\n            var _sizeLocal = (LocalBuilder?)null;\r\n            LocalBuilder GetSizeLocal() => _sizeLocal ??= il.DeclareLocal(typeof(int));\r\n            il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param]\r\n\r","sourceCodeStart":2544,"sourceCodeEnd":2580,"githubUrl":"https://github.com/DapperLib/Dapper/blob/72a54c475f75e18cb93cba0809d00a5e6e49efd9/Dapper/SqlMapper.cs#L2544-L2580","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","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."],"exampleFix":"// before\nvar rows = cnn.Query<T>(\"select * from t where id = @Id\", (Id: 1));\n// after\nvar rows = cnn.Query<T>(\"select * from t where id = @Id\", new { Id = 1 });","handlingStrategy":"validation","validationCode":"// Convert tuples to anonymous objects before passing to Dapper.\nvar p = new { Id = tuple.Id, Name = tuple.Name };\nvar rows = cnn.Query<T>(sql, p);","typeGuard":"static bool IsValueTupleParam(object? p) => p?.GetType().FullName?.StartsWith(\"System.ValueTuple`\", StringComparison.Ordinal) == true;","tryCatchPattern":null,"preventionTips":["Use anonymous types or named classes for parameters.","Never pass a ValueTuple as the param object.","Project tuples into anonymous objects at the boundary."],"tags":["dapper","valuetuple","parameters","reflection"],"backgroundTag":null,"analyzedSha":"72a54c475f75e18cb93cba0809d00a5e6e49efd9","analyzedAt":"2026-08-13T14:18:18.115Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}