Skip to content

Support DynamicParameters by delegating to the bag's own protocol - #195

Merged
mgravell merged 6 commits into
mainfrom
dynamicparameters-support
Aug 20, 2026
Merged

Support DynamicParameters by delegating to the bag's own protocol#195
mgravell merged 6 commits into
mainfrom
dynamicparameters-support

Conversation

@mgravell

Copy link
Copy Markdown
Member

A DynamicParameters argument is runtime state — no generator can know its members. But the bag already implements the entire vanilla protocol (per-parameter settings, templates, literal replacement, RemoveUnused, storing attached parameters so Get<T> reads outputs), so the generated command factory simply delegates: args.AddParameters(cmd.Command!) via the new identity-free overload (DapperLib/Dapper#2225), plus a PostProcess firing IParameterCallbacks.OnCompleted. Because it is vanilla's code path, behavior is exact — including templates on JIT; under native AOT templates fail inside Dapper's own ref-emit exactly as vanilla does there.

Probe-gated: the generator looks for the AddParameters(IDbCommand) symbol on the referenced Dapper and only takes this path when present — older Dapper keeps the DAP015 refusal, so the entire golden suite is untouched against the current package, and this can merge independently of the Dapper PR. Dynamic bags route to the deferred parameter map (command caching disabled); one mode fix included: stored procedures previously chose ParameterMode.All before the bag Defer check, which handed proc+bag call-sites the parameterless fallback factory.

Measured on the Dapper test suite (with the Dapper-side overload via project reference): 494 → 533 of 725 call-sites handled; DAP015 drops 114 → 30 (the remainder are object-typed arguments, which stay refused); the ProcedureTests DynamicParameters group (output/return params, Get<T>, DateTime2 precision) goes fully green.

A DynamicParameters argument is runtime state - no generator can know
its members. But the bag already implements the entire vanilla protocol
(per-parameter DbType/direction/size/precision/scale, templates, literal
replacement, RemoveUnused, storing attached parameters so Get<T> reads
outputs), so the generated command factory simply delegates:
AddParameters(cmd) via the new identity-free Dapper overload, plus a
PostProcess that fires IParameterCallbacks.OnCompleted. That is vanilla's
own code path, so behavior is exact - including templates on JIT; under
native AOT templates fail inside Dapper's ref-emit exactly as vanilla
does there.

The generator probes for the AddParameters(IDbCommand) symbol and only
takes this path when the referenced Dapper has it - older Dapper keeps
the DAP015 refusal, so nothing changes for existing consumers (the full
golden suite is untouched against the current package). Dynamic bags
already route to the deferred parameter map, which disables command
caching; collection/multi-exec detection is skipped for them.

On the Dapper test suite (with the Dapper-side overload): 494 -> 533 of
725 call-sites handled; DAP015 drops 114 -> 30 (the remainder are
object-typed arguments, which stay refused - announced-types territory).
The Defer decision for DynamicParameters-style bags only applied on the
command-text branch; stored procedures took ParameterMode.All first, so
a proc+bag call-site got an empty map and selected the parameterless
fallback factory - parameters never attached ('expects parameter @id,
which was not supplied'). Caught by the Dapper test suite's proc tests
on the first behavioral run; they go 16 failures to 0 with this (the
two remaining in that file are the known list-expansion gap, unrelated).
Marc's rule for Dapper-side dependencies, now established as the pattern:
probe for the symbol, never emit code that cannot compile against the
referenced Dapper, and refuse with a diagnostic that names exactly which
API is missing and what it enables - not a generic message, and never a
baffling compiler error. The DynamicParameters path previously fell back
to DAP015 ('parameter type could not be resolved'), which described the
symptom rather than the fix; it now reports DAP052 naming
DynamicParameters.AddParameters(IDbCommand). Docs page included; the
verifier runs against the packaged (older) Dapper, which is exactly the
scenario the diagnostic exists for.
@mgravell

Copy link
Copy Markdown
Member Author

Added DAP052 per the feature-detection rule: when a Dapper.AOT feature depends on an API in the referenced Dapper (here DynamicParameters.AddParameters(IDbCommand)), the generator probes for the symbol; if absent, the call-site is left on vanilla Dapper with a diagnostic that names exactly which API is missing and what it enables — never generated code that cannot compile, and never a generic message. The verifier for it runs against the packaged (older) Dapper, which is precisely the scenario the diagnostic exists for. This is the pattern for any future Dapper-side dependencies too.

The verifier previously relied on the project-wide Dapper reference not yet
exposing AddParameters(IDbCommand) - which means a future package bump
silently flips the probe and fails this test. It now references the shipped
2.1.72 package explicitly (a new pinDapperPackageVersion knob on the
verifier, which swaps the live assembly for a resolved package), so it
guards the probe-and-refuse path permanently, against the genuine old
artifact. When the bump happens, the remaining work is the positive twin
(same code, live reference, no diagnostic) and a golden fixture for the
defer emit - both impossible to write until a released Dapper has the API.
@mgravell
mgravell merged commit 419c9df into main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant