Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mgravell opened this issue Jun 8, 2023 · 13 comments
Open
Assignees
Labels
announcement area:api API Additions or Changes v3.0 Changes awaiting the next breaking release
Milestone

Comments

@mgravell
Copy link
Member

mgravell commented Jun 8, 2023

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:.

  • hard to debug at runtime
  • lots of runtime work, both initially (emitting strategies) and each time (strategy cache lookups, metadata verification, etc)
  • hard to develop, with few people able to contribute
  • in particular, hard to extend async code, since ref-emit and async are not good friends
  • expensive to maintain - a "small" change can have huge impact to the ref-emit code, and cause instability
  • absolutely categorically does not work with AOT or other ref-emit constrained runtimes

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?

  • this AOT approach should be the future of Dapper
  • we should continue to have the ref-emit internals and existing implementation, but I do not plan any new feature development in the ref-emit code
  • the AOT mode should usually not require any code-changes, but a new API will also be available (in particular for use in wrapper data-access layers); your existing code should just work
  • things that currently require additional runtime configuration may need some new annotation-based (or similar) approach; such features may not be available initially (emphasis: AOT mode is configurable at any granularity, so: you can opt-out if needed for some scenarios)
  • we should add the missing IAsyncEnumerable<T> support in the main library (this is already supported in the AOT demo) - update: done
  • we should prioritize (in the net8 time-frame) getting the AOT mode deployed as an optional opt-in feature
  • initially there may be some feature variance between the two, which we will need to work on
  • new features will be added via the AOT mode only, where the impact is easy to understand and validate (by virtue of being able to read the generated code)
  • the analyzer, when enabled, should be given the optional ability to report on times it wasn't able to rewrite a method, explaining why
  • no non-beta previews until "interceptors" ships
  • the AOT mode should target the full breadth of TFMs when possible, but it is acknowledged that using "interceptors" is dependent on using up-to-date compilers and build tools
  • but without them, your existing code should still work unchanged in the ref-emit style
  • this work basically becomes the vNext of Dapper, probably with a "major" denoting the step change (although no breaking API shape); as above, any vNext features would be based on the new code approach

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

@mgravell mgravell added v3.0 Changes awaiting the next breaking release announcement area:api API Additions or Changes labels Jun 8, 2023
@mgravell mgravell added this to the v3.0 milestone Jun 8, 2023
@mgravell mgravell self-assigned this Jun 8, 2023
@T-Gro
Copy link

T-Gro commented Jun 8, 2023

Iterators => interceptors ?

@mgravell
Copy link
Member Author

mgravell commented Jun 8, 2023

Dammit! Fixing - brainfart

@michal-ciechan
Copy link

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
Copy link
Contributor

Tornhoof commented Jun 8, 2023

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
Copy link

giammin commented Jun 8, 2023

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
Copy link

T-Gro commented Jun 8, 2023

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 dotnet/fsharp#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
Copy link
Member Author

mgravell commented Jun 8, 2023

@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
Copy link
Member Author

mgravell commented Jun 8, 2023

@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
Copy link
Member Author

mgravell commented Jun 8, 2023

@giammin

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

Happy to over-deliver :)

@mgravell
Copy link
Member Author

mgravell commented Jun 8, 2023

@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
Copy link
Member Author

mgravell commented Jun 8, 2023

@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
Copy link
Member Author

mgravell commented Jun 8, 2023

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
announcement area:api API Additions or Changes v3.0 Changes awaiting the next breaking release
Projects
None yet
Development

No branches or pull requests

6 participants