Proposal: Custom Deserializer Methods
Background
Dapper allows custom Type Handlers and Type Mappers to be provided in order to get the required output from your database. Both of these constructs are used when Dapper dynamically generates deserializer methods using reflection and IL generation.
Whilst this functionality works well 99% of the time, there are certain instances where some more control over deserialization would be beneficial, e.g. a data object containing an enum which cannot be handled by Dapper's current enum handling (a long standing-request).
Proposed Solution
In much the same way that Type Handlers and Type Maps can be configured currently, a new method could be added AddTypeDeserializer<T>(Func<DbDataReader, T>) to allow the custom Type Deserializer to be configured. This custom Type Deserializer would then be returned by GetTypeDeserializer(Type, DbDataReader, int, int, bool) and take full responsibility for creating the result object from the data in the DbDataReader. It would be expected to know how to read the required data from the DbDataReader, regardless of what database query led to its execution since it's provided by the user who is also responsible for the database queries being issued.
Points to consider
- Custom Type Deserializers should work in much the same way as the dynamically generated Type Deserializers currently emitted by Dapper and so would not allow customisation of simple scalar results that are handled differently.
- Custom Type Deserializers could be generated by a Source Generator, removing the need for dynamically generated IL
This is already planned; over in the AOT piece, we already have code-gen to swap out full end-to-end usage scenarios that the AOT layer can handle, but it can't currently handle more general <T> scenarios. To help here, it is planned to add "some API" to allow registration of pieces so that even if the AOT piece can't swap end-to-end, it can still contribute enough that there is no runtime ref-emit needed.
This will almost certainly start with attributes so that a generator can just spit out additional attributes (*), but a secondary API ala your AddTypeDeserializer: certainly possible.
The * here is "where"; partial types allow addition of extra attributes to the DTO, but that assumes the DTO is in the assembly being compiled - which is not necessarily the case. The same applies to module/assembly-level markers: which modules/assemblies should Dapper check? In the "different assembly" scenario, this isn't obvious; maybe "all currently loaded" is the way to go there. For brevity, then, in order:
- check the existing cache
- check the DTO type for an attribute
- check all loaded assemblies for module-level attributes about that DTO
- use reflection and ref-emit
Sound about right?
Is the new API that is planned intended for Dapper or Dapper AOT? The reason I ask is that I have a few uses of QueryMultipleAsync so the AOT project isn't a drop-in replacement for me at this point.
It would be a new API in Dapper vanilla, that Dapper.AOT then uses, i.e. by adding annotations that are basically "I saw this type and generated a deserializer for it; hey Dapper, you can now use this for any remaining code paths that I (Dapper.AOT) couldn't rewrite completely"