DapperLib / Dapper

Dapper - a simple object mapper for .Net
https://www.learndapper.com/
Other
17.44k stars 3.67k forks source link

Announcement/Input - DapperAOT progress and Dapper API / feature update #1909

Open mgravell opened 1 year ago

mgravell commented 1 year ago

There's a lot of preamble and back-story here. If you're short of time, scroll down to "Call to action".


You would be forgiven for thinking

The devs aren't iterating Dapper any more, is it dead?

I'm happy to say that the answer is an emphatic "no, it isn't dead", and "yes, we are still here" - and while ADO.NET isn't quite as central to our current roles at Microsoft as it was when we worked at Stack Overflow, it underpins a lot of the teams that use the things that we (@mgravell and @NickCraver) work on.

So: I've been a little hesitant to invest heavily in extending Dapper in its current form, for a few reasons, mostly related to metaprogramming. A lot of my thoughts are captured here. The key impact of this is:.

I've wanted to improve that state. Initially I spent a while looking at a "reimagining of Dapper" that used the extended partial methods support in C# 9? 10? and "generators", so that an analyser understood the intent of your code (which looked very different to Dapper today), and wrote the missing pieces. It worked, but it was inelegant, and frankly "just use this completely different API" is a terrible story re adoption.

But last week, I had an epiphany. I learned about "interceptors", which are hopefully going to be released in net8. Interceptors are a new C# vNext feature that allow additional code (typically via a generator) to say "that method call there? yeah, just ignore where that's going - come here instead" - effectively, it allows code to retarget what arbitrary method calls do.

So how is this relevant to Dapper? Well, if an analyzer can find your connection.QueryAsync<Customer>(...) call, it can spit out an interceptor for that specific call (or multiple call-sites if it wants), that is a method that achieves the same result (probably forwarding the same arguments, unless it wants to optimize), but using an API that allows generated code to deal with all the parameter packing and row parsing. To make a long story short ("too late!"), it makes your existing Dapper code work without requiring any ref-emit at runtime, with zero ongoing strategy cache checks, in a fully AOT way.

I have a proof-of-concept that has this working, with regular Dapper code that is made AOT-friendly just by adding a build package. It works with all the simple scalar, non-query, and typed query methods in both their synchronous and asynchronous forms - showing that the idea is sound. With tests that make it trivial to see what code is generated for a given input. There's also consideration of opt-out, and scenarios where the query is dynamic and being passed down (for wrapper data access layers) - although this area is not yet fully developed. Oh, and did I mention that the new implementation can be used in a mockable way (†)?

Call to action

Sounds great? I think so, at least. So, my thinking is - and this is where I want input; does the following sound reasonable?

That's it; that's the update and the "does this sound reasonable?". Now your turn; your thoughts please!


† = using the mockable mode requires using slightly different code - which is still unmistakably Dapper - and requires an object allocation (think "box") which is not needed when using the non-mockable mode

T-Gro commented 1 year ago

Iterators => interceptors ?

mgravell commented 1 year ago

Dammit! Fixing - brainfart

michal-ciechan commented 1 year ago

Sounds good, and thanks for letting us know Dapper isn't dead!

I would be tempted to say that the new Interceptor should be behind another API and not a drop in replacement, what if you want to use the AOT aspects of it, but there is a bug in a specific method call that it is replacing, can we easily switch back for a single call? (Maybe additional optional parameter for strategy enum flags?)

How do we know something will be AOT'ed vs it won't/can't? (E.g. Silent performance degradation when someone makes a change that is no longer compatible with AOT and falls back to ref emit)

If you have different APIs would it be easier to split the API into different libraries, therefore knowing explicitly which is AOT and which is ref emit?

Tornhoof commented 1 year ago

Personal opinion? Clean aot only version and then think about interceptors for later migration of existing Code.

Why? Interceptors are a sufficiently black-magic and new Feature. Looking back at srcGens, it took a few versions and iterations until they were usable and maintainable. I kinda think it will be the same for interceptors. I simply would not yet bet on that horse.

giammin commented 1 year ago

this is awesome! I expected DapperAOT to be a complete api rewrite with no compatibility/interoperability with the ref-emit version. And I was ok with that, it would be totally acceptable

T-Gro commented 1 year ago

I came here to share the perspective of 'minority-languages-on-dotnet' (and to link this to an issue where I am collecting use cases exactly like this one :-) ):

Maybe not every reader of this post will know - the combination of source generators and interceptors is a set of Roslyn compiler features, not a .NET feature. Since the codegen is essentialy "C# in a string", it means that the support brought by it, incl. vNext features Marc mentions, will not work in non-C# projects such as F#. Those compilers will not understand the interception, and will continue to use the old code path (=fallback).

We do have an open discussion about the topic in the F# compiler repo https://github.com/dotnet/fsharp/issues/14300 , and popular projects like Dapper become the ammunition to continue thinking about it. Even more so if it comes with improved perf or opens relevant scenarios (AOT) - for important libraries, this can be a decision-making factor for choosing a language.

mgravell commented 1 year ago

@michal-ciechan

if you want to use the AOT aspects of it, but there is a bug in a specific method call that it is replacing, can we easily switch back for a single call?

already implemented; whack [DapperAot(true)] or [DapperAot(false)] at any level: job done

How do we know something will be AOT'ed vs it won't/can't?

Already mentioned the intention to emit an analyzer output when it can't (and not disabled); if people don't care, they can suppress/disable that output

therefore knowing explicitly which is AOT and which is ref emit?

As above, but also discoverability of interceptors should hopefully also be an IDE feature in due course

mgravell commented 1 year ago

@Tornhoof

Clean aot only version and then think about interceptors for later migration of existing Code.

Right; to explain - there is a secondary API which the generators use under the hood (and which you'd need to use from "common data access methods" etc), but: that secondary API still expects other generated code, because we need all the magic to write commands, parse records, etc. So: unless you're going to hand write all that, we either need interceptors, or a global per-type handler registry, and I hate per-type registries. Basically: if you don't want to use AOT, maybe just don't? Either don't install the AOT tooling, or don't enable it (it is opt-in/opt-out at any level)

mgravell commented 1 year ago

@giammin

I expected DapperAOT to be a complete api rewrite with no compatibility/interoperability with the ref-emit version.

Happy to over-deliver :)

mgravell commented 1 year ago

@T-Gro

will not work in non-C# projects such as F#. Those compilers will not understand the interception

Yes, I'm very conscious of that. And to be clear, I'm not proposing to take anything away: the existing ref-emit core should still work exactly the same - no code changes required there either.

For other Roslyn-enabled languages (by which I guess I mean VB), if they support interceptors, and if someone wants to help with the work (my VB is mostly read-only these days), that's certainly something we could look at.

For non-Roslyn languages, in particular F#: if there are similar code-voodoo tools or tricks available, I'm happy to help by showing what the API needs in order to function (although the test output files literally show that, so...). I'm not an expert in those areas, but I will help in any way I can. I gather that F# does have some build-time voodoo ("providers" or something?). I claim zero expert knowledge there, but again: happy to help.

mgravell commented 1 year ago

@T-Gro I should also emphasize: even if there is no "interceptors" metaphor that would allow (some language) to use the regular Dapper API: the secondary API is - well, honestly it is better than the primary API, and should be very accessible to use from other languages without needing interceptors but you'd still need something to generate "here's my row reader". To explain: in the new API the idea is that this part (the reader) is optional and defaults to null - the generator intercepts those calls and performs the same call, but providing the missing parameter. But if (some language) provided that parameter from somewhere else - presumably generated in (some language) - just involves subclassing and a few overrides, that would work fine too: full AOT.

mgravell commented 1 year ago

Re "other language support": here's a point-in-time example of the proposed secondary/new API that intercepts a parameterized typed query (do not quote me on the shape!) - the command-factory and row-reader are generated and are the bits that would need to also be emitted by the per-language tool: https://github.com/DapperLib/DapperAOT/blob/2d7f1ea6454c70a3c57a54e7f730069db805848c/test/Dapper.AOT.Test/Interceptors/Query.output.cs#L17

The code is verbose for code-gen reasons: I don't like using directives in generated code, due to conflict risks

ivmazurenko commented 4 months ago

I always wanted to use something like dapper in amazon lambdas. I tried to implement minimal code generation for simple mappings. Maybe somebody will be interested and gived some concerns about this idea