Type handlers: runtime SqlMapper.AddTypeHandler registrations honored end-to-end - #206
Type handlers: runtime SqlMapper.AddTypeHandler registrations honored end-to-end#206mgravell wants to merge 4 commits into
Conversation
…announced attributes
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.
Enum parameters are common, and the runtime dispatch was charging them a dictionary lookup per execution for a feature that only applies under Settings.PreferTypeHandlersForEnums (vanilla consults enum handlers only under that setting too). The lookup is now guarded by the static bool, so enum parameters pay a single field read unless the feature is actually in use; on a Dapper too old to have the setting, enums keep the fully baked path (probe-gated, like the rest). Also records the agreed direction in the design note: declarative [module: ...] config attributes as the primary spelling - better scoped than the process-global registries they replace - with the runtime bridge as compatibility and a strict switch to disable it outright.
|
Two additions from the design discussion: enum parameters no longer pay the lookup unless |
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 seeAddTypeHandlercalls at compile time, so generated code defers to vanilla's own decision procedure at execution time:SqlMapper.LookupDbType(public, CS0618-suppressible — the same tier asPackListParameters). Handler present →handler.SetValue, passingDBNullfor null (never null itself —TypeHandler<T>'s interface impl NREs on the struct cast; probed, and vanilla coalesces too). Otherwise any returnedDbTypeapplies and the value binds raw exactly as before (demand: falsedeliberately: 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).PackageReference). Generated code installs aTypeHandlerBridgefrom 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-gatedModuleInitializerAttributepolyfill; the whole feature is inert against a Dapper too old to haveHasTypeHandler/LookupDbType.char/char?andobject/dynamicstay excluded from dispatch (padding and runtime-typing respectively);ParamMembergainsTypeOfNamemirroringRowMember's, becausetypeofon an annotated reference type (CS8639) ordynamic(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
PreferTypeHandlersForEnumstest —LookupDbTypechecks that setting internally, so it came free), plus the bare-DataTableTVP pair and the Xml tests, because vanilla registersDataTableHandlerand the XML handlers by default and the dispatch reaches them like any registration.Deliberately not here (recorded in the design note):
AddTypeMapon recognized scalars (the string→AnsiString tests — honoring it costs a per-parameter lookup on the hottest types; explicit trade to discuss), and theSetTypeMapruntime 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).