Skip to content

Type handlers: runtime SqlMapper.AddTypeHandler registrations honored end-to-end - #206

Open
mgravell wants to merge 2 commits into
mainfrom
typehandlers
Open

Type handlers: runtime SqlMapper.AddTypeHandler registrations honored end-to-end#206
mgravell wants to merge 2 commits into
mainfrom
typehandlers

Conversation

@mgravell

Copy link
Copy Markdown
Member

The biggest remaining behavioral class. Design and probed facts in notes/typehandlers-design.md (committed here); the short version is the delegate-to-vanilla doctrine applied to runtime-mutable state — the generator cannot see AddTypeHandler calls at compile time, so generated code defers to vanilla's own decision procedure at execution time:

  • Writes: a member type the generator doesn't recognize dispatches through SqlMapper.LookupDbType (public, CS0618-suppressible — the same tier as PackListParameters). Handler present → handler.SetValue, passing DBNull for null (never null itself — TypeHandler<T>'s interface impl NREs on the struct cast; probed, and vanilla coalesces too). Otherwise any returned DbType applies and the value binds raw exactly as before (demand: false deliberately: modern providers natively handle types vanilla's map doesn't — DateOnly, until the Dapper re-enable ships). Expandable members check the handler first — a handled collection type must not list-expand (Issue253), which is the order vanilla applies. Update-mode mirrors the dispatch, so command reuse stays legal (the parameter shape is stable).
  • Reads: the lib deliberately doesn't reference Dapper (a consumer may use Dapper or Dapper.StrongName, and a hard reference would load both and split the handler registry — I believe this is why the csproj has that commented-out PackageReference). Generated code installs a TypeHandlerBridge from a module initializer, compiled against the consumer's own Dapper; the flexible read path consults it, and a whole-type handler overrides a generated row factory (RowFactory<T>.Resolve), matching vanilla's handler-before-member-binding order. Down-level targets get a probe-gated ModuleInitializerAttribute polyfill; the whole feature is inert against a Dapper too old to have HasTypeHandler/LookupDbType.
  • char/char? and object/dynamic stay excluded from dispatch (padding and runtime-typing respectively); ParamMember gains TypeOfName mirroring RowMember's, because typeof on an annotated reference type (CS8639) or dynamic (CS1962) doesn't compile — found the hard way when the harness build failed silently.

Dapper test suite: 677 → 705/793. The entire runtime-handler family clears (Issue136, Issue1959 ×4, Issue253 ×2, Issue461, SO24740733 ×2, Issue149, the PreferTypeHandlersForEnums test — LookupDbType checks that setting internally, so it came free), plus the bare-DataTable TVP pair and the Xml tests, because vanilla registers DataTableHandler and the XML handlers by default and the dispatch reaches them like any registration.

Deliberately not here (recorded in the design note): AddTypeMap on recognized scalars (the string→AnsiString tests — honoring it costs a per-parameter lookup on the hottest types; explicit trade to discuss), and the SetTypeMap runtime column-mapping family (the parity table's 🚫 proposal stands). Prior art #117 (samcragg) and #162 (7amou3) are the announced-attribute tier — the right shape for static dispatch, and still worth doing as an optimization on top of this, redone on the plain-data model (#162's symbol-keyed registry predates phase 2).

SqlMapper.AddTypeHandler registrations now work under interception, in both
directions, by delegating to vanilla's own decision procedure at execution
time rather than trying to see runtime state at compile time:

- writes: a member type the generator does not recognize emits a dispatch
  through SqlMapper.LookupDbType (public, CS0618-suppressible - the same
  tier as PackListParameters): handler present -> handler.SetValue (with
  DBNull for null, never null itself - the TypeHandler<T> interface impl
  NREs on a raw null); otherwise any returned DbType is applied and the
  value binds raw as before (demand:false deliberately - modern providers
  natively handle types vanilla's map does not, DateOnly being the live
  case until the Dapper re-enable ships). Update-mode mirrors it, so
  command reuse stays legal (the parameter shape is stable); an expandable
  member checks the handler *first*, which is the order vanilla applies
  (a handled collection type must not list-expand - Issue253).
- reads: the lib cannot reference Dapper (a consumer may use Dapper or
  Dapper.StrongName, and a hard reference would split the handler registry
  between the two), so generated code installs a TypeHandlerBridge from a
  module initializer, compiled against the consumer's own Dapper; the
  flexible read path consults it, and a whole-type handler overrides a
  generated row factory (RowFactory<T>.Resolve), matching vanilla's
  handler-before-member-binding order. A ModuleInitializerAttribute
  polyfill is emitted for down-level targets, probe-gated like the
  interceptor attribute; the whole feature is inert against a Dapper too
  old to have HasTypeHandler/LookupDbType.
- char/char? stay excluded (their StringFixedLength map entry pads the
  round-trip); object/dynamic stay excluded (runtime-typed values);
  ParamMember gains TypeOfName, mirroring RowMember's, because typeof on
  an annotated reference type or dynamic does not compile.

This also clears the bare-DataTable TVP shape and the Xml types for free -
vanilla registers DataTableHandler and the XML handlers by default, and the
dispatch reaches them like any other registration.

Dapper test suite: 677 -> 705/793. Design and probed facts in
notes/typehandlers-design.md; prior art PRs #117 and #162 (the announced-
attribute tier) remain as the static-dispatch optimization, redone on the
plain-data model.
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