Support ICustomQueryParameter members (TVPs): the value adds itself - #198
Merged
Conversation
A member implementing SqlMapper.ICustomQueryParameter (a TVP from AsTableValuedParameter being the common case, on a DataTable or an IEnumerable<SqlDataRecord>) previously bound as a single raw parameter; now the generated AddParameters calls value.AddParameter(command, name), which is the whole vanilla contract. A null reference-typed member throws with vanilla's exact message; a struct member gets no null test. The command cannot be prepared (no declared DbType). The list-expansion guards generalise to cover this: expandable and custom members are both 'self-binding' - they contribute an unknowable number of parameters - so the same parse-side rules apply (no command caching; skip multi-exec; skip alongside output/return parameters, which PostProcess reads back by index). The shared p local pre-scan learns the new member kind too. New CustomParameters fixture covers interface-typed, class and struct members, a custom member alongside a plain one, and both skips.
mgravell
added a commit
that referenced
this pull request
Aug 20, 2026
* Support list expansion (where X in @ids) by delegating to Dapper's own PackListParameters An expandable (enumerable) member previously bound as a single raw parameter, which fails at execution; now the generated AddParameters delegates to the public (obsolete, "library usage only") SqlMapper.PackListParameters, which owns the whole in-list contract: the SQL rewrite (including the empty-list and optimize-hint forms), per-item parameters, DbString items, padding and string_split settings, and provider array support. Calling the existing API means this works against every shipped Dapper, so no feature-detection diagnostic is needed; the alternative - a fresh non-obsolete wrapper in Dapper, probe-gated like the DynamicParameters overload - is a fair follow-up if we would rather not lean on an [Obsolete] member from generated code. Guard rails, all parse-side so the call-sites stay on vanilla Dapper rather than misbehave: - command caching is disabled for a factory with an expandable member (the parameter shape varies per call), and CanPrepare is cleared; - multi-exec over elements with an expandable member is skipped (batch reuse updates parameters in-place, which cannot re-expand a list whose size changed); - an expandable member alongside an output/return parameter is skipped (PostProcess reads those back by index, and expansion shifts every index after it). The shared "p" local in AddParameters is now emitted only when some member still needs it, since a factory whose members all expand otherwise declares it unused (CS0168 in the consumer's build). New ListExpansion fixture covers the three working shapes and both skips; the TsqlTips golden moves off the raw bind, which was the broken behaviour. * Tick the parity cells this lands * Support ICustomQueryParameter members (TVPs): the value adds itself (#198) * Support ICustomQueryParameter members: the value adds itself A member implementing SqlMapper.ICustomQueryParameter (a TVP from AsTableValuedParameter being the common case, on a DataTable or an IEnumerable<SqlDataRecord>) previously bound as a single raw parameter; now the generated AddParameters calls value.AddParameter(command, name), which is the whole vanilla contract. A null reference-typed member throws with vanilla's exact message; a struct member gets no null test. The command cannot be prepared (no declared DbType). The list-expansion guards generalise to cover this: expandable and custom members are both 'self-binding' - they contribute an unknowable number of parameters - so the same parse-side rules apply (no command caching; skip multi-exec; skip alongside output/return parameters, which PostProcess reads back by index). The shared p local pre-scan learns the new member kind too. New CustomParameters fixture covers interface-typed, class and struct members, a custom member alongside a plain one, and both skips. * Tick the parity cells this lands
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #197 (same Add-mode switch and the same guard family) — review that one first; this diff is small on top of it.
A member implementing
SqlMapper.ICustomQueryParameter— a TVP fromAsTableValuedParameter(on aDataTableor anIEnumerable<SqlDataRecord>) being the common case — previously bound as a single raw parameter. The generatedAddParametersnow callsvalue.AddParameter(command, name), which is the whole vanilla contract; a null reference-typed member throws with vanilla's exact message (inlined rather than via the obsoleteThrowNullCustomQueryParameter, so no version dependency), and a struct member gets no null test.CanPrepareis cleared since the parameter declares noDbType.The #197 guards generalise: expandable and custom members are both self-binding — they contribute an unknowable number of parameters — so the same parse-side rules apply: no command caching, skip multi-exec, and skip alongside output/return parameters (
PostProcessreads those back by index).Note a bare
DataTablemember (withoutAsTableValuedParameter) is not covered here: vanilla routes that through its default-registeredDataTableHandler, so it belongs to the type-handler story.New
CustomParametersfixture covers interface-typed, class and struct members, a custom member alongside a plain one, and both skips. Against the Dapper test-suite harness this clears the TVP/custom-param group in ParameterTests on both SqlClient providers.