Skip to content

feat: TypeHandler support - #162

Open
7amou3 wants to merge 1 commit into
DapperLib:mainfrom
7amou3:main
Open

feat: TypeHandler support#162
7amou3 wants to merge 1 commit into
DapperLib:mainfrom
7amou3:main

Conversation

@7amou3

@7amou3 7amou3 commented Jul 20, 2025

Copy link
Copy Markdown

This PR represents an attempt to resolve issue #159.

The prior approach of the source generator (#117) inadvertently led to the generation of code that instantiated a new TypeHandler object for each parameter or column read operation (new global::CustomClassTypeHandler().Read(...)). This could lead to unnecessary object allocations and a minor performance overhead, especially in high-volume scenarios.

This PR introduces a TypeHandlerInstanceRegistry and modifies the code generation to ensure that:

  • A single, static instance of each registered TypeHandler is created within the generated interceptor file (e.g., __Handler1, __Handler2).
  • All subsequent operations (setting parameters, parsing output parameters, reading result set columns) for a given type handler utilize this pre-allocated, static instance.

Comment thread docs/typehandlers.md
// Example using a module-level attribute
using Dapper;

[module: TypeHandler<YourDotNetType, YourCustomTypeHandler>]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have a query that required certain set of typeHandlers,
And other do not,
Then how to restore them, and then add if and when needed ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, this PR does not currently allow for switching or disabling type handlers on the fly.
This is definitely a feature that could be added, though.

@martinflorek

Copy link
Copy Markdown

Any update on this PR or #117? I am debugging custom Dapper.AOT TypeHandler for days and it looks like they don't work at all in the lastest Dapper,AOT 1.0.48?

I have registered them via

[module: DapperAot]
[module: TypeHandler<MyCustomType?, MyCustomTypeHandler>]

and they are not used in source generation and I just get runtime cast errors when loading data from DB "Invalid cast from 'System.Int64' to 'MyCustomType'."

What is the purpose of [module: TypeHandler<MyCustomType?, MyCustomTypeHandler>], am I misunderstanding its purpose?

mgravell added a commit that referenced this pull request Aug 23, 2026
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.

3 participants