Skip to content

Add Async Query Support #110

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

Merged
merged 26 commits into from
Sep 26, 2020
Merged

Add Async Query Support #110

merged 26 commits into from
Sep 26, 2020

Conversation

Turnerj
Copy link
Member

@Turnerj Turnerj commented Nov 4, 2019

Closes #26

Add support for async queries, specifically async versions of these:

  • First/FirstOrDefault
  • Single/SingleOrDefault
  • ToList
  • ToArray
  • Any
  • Count
  • Max
  • Min
  • Sum

Additionally, support IAsyncEnumerable<T> via AsAsyncEnumerable.

Each item in the list above needs custom handling to inject the right expression into the MongoDB driver while also triggering the async execution path on the MongoQueryProvider. If you simply call AsAsyncEnumerable().FirstOrDefaultAsync(), that will not filter the query properly and will make the DB return more than one item (as it batches items internally). Similarly, if you ran AsAsyncEnumerable().SumAsync(), it would return all of the items from the DB and do the sum locally. Fundamentally this problem exists as there is no such thing as an async Queryable and all enumerables are performed locally.

The effort involved in supporting this process makes it possible to side step the built-in result transformer logic of the MongoDB driver which is a good thing for MongoFramework as the only way to call it was via reflection (it is an internal type we need access to).

This will have full support for .NET Standard 2.0.

Original Description

This adds IAsyncEnumerable<TOutput> support to MongoFramework, allowing for full async/await paths.

Support will likely be shaped around Ix.NET though will likely need to factor in the same issues that EF Core have had.

The ideal end result is full backwards compatibility for IAsyncEnumerable even where it isn't available as standard (.NET Standard 2.0). If this is not possible, async query support will be limited to .NET Standard 2.1 and above.

@Turnerj Turnerj added enhancement Enhancements & features affects-querying Query-related issue moderate-change Moderate changes required labels Nov 4, 2019
@Turnerj Turnerj self-assigned this Nov 4, 2019
@Turnerj Turnerj added this to the 1.0.0 milestone Aug 17, 2020
@JohnCampionJr
Copy link
Contributor

Curious as to what needs to be done to get this merged (beyond updating the branch to the latest). The code seems to work well. Happy to help!

@Turnerj
Copy link
Member Author

Turnerj commented Sep 21, 2020

I'm curious too - I don't remember why I didn't merge it. I think there was something weird with extensions on IEnumerable<T> and IAsyncEnumerable<T>, potentially conflicting? Wish I wrote notes down!

Probably will need to add a few more tests at minimum as I know I had some issues previously with wrapping the "Result Transformer" on the sync-path, probably have the same issue on the async-path.

I'll give it a look again - maybe today - and see what is actually left.

@Turnerj
Copy link
Member Author

Turnerj commented Sep 21, 2020

I worked out why I didn't merge the change, it only added async support for enumerating, not for FirstOrDefault, Count etc. The reason the distinction is important is that when you call AsAsyncEnumerable, it has actually started the query to the DB so if you did myQueryable.AsAsyncEnumerable().CountAsync(), that count would be done locally (async fetching all the entities then counting it), not on the DB.

I need to manually add every async extension myself on IQueryable like Entity Framework does.

I'll add a few big ones and add the rest ad-hoc:

  • FirstAsync (with and without expression)
  • FirstOrDefaultAsync (with and without expression)
  • SingleAsync (with and without expression)
  • SingleOrDefaultAsync (with and without expression)
  • ToListAsync
  • ToArrayAsync
  • AnyAsync (with and without expression)
  • CountAsync (with and without expression)

Any others after that should be fairly simple PRs just following the same pattern.

@Turnerj
Copy link
Member Author

Turnerj commented Sep 21, 2020

Maybe not as simple as I was hoping - the result transformer logic for async in the driver is different from sync. Specifically for sync, it uses the Enumerable extension methods, taking an IEnumerable in. For async, it is expecting to take an IAsyncCursor which I don't have because of how I need to intercept the logic. Instead I have an IAsyncEnumerable.

All in all, I basically need to make my own result transformers but that isn't actually too bad as I might end up avoiding some reflection logic.

@JohnCampionJr
Copy link
Contributor

I spent a chunk of time yesterday trying to understand the internals of the Async support. I got as far as the ResultTransformer and I understand what you're doing with the First, but have no idea how to help implement more. Sorry....

@Turnerj
Copy link
Member Author

Turnerj commented Sep 25, 2020

No worries - I think I have a good strategy for the different methods etc now. Also learnt that the MongoDB driver doesn't support Last or LastOrDefault which is fine for me - I never use them myself plus I can remove a bunch of code from my new async path.

Turnerj and others added 14 commits September 25, 2020 19:08
Note: This commit removes .NET Standard 2.0 support however that won't be the case in the future - .NET Standard 2.0 support will still exist, this was just to get some basic tests running without needing to do a ton of ifdef statements.
The consuming code no longer needs to cast to `IMongoFrameworkQueryable<TOutput>` itself
Successfully have FirstAsync working through the full async pipeline. Pretty much broke everything else though...
New MethodInfo cache for easy access during expression building.
Fleshed out the ResultTransformers logic for the different enumerable handling operations.
Additional tests covering First, FirstOrDefault, Single and SingleOrDefault.
Previous type detection was invalid for the type of data returned in the pipeline. This now reflects closer to what the Driver does internally.
These are simple passthrough extensions. For anything more complex, a user can go directly to the IAsyncEnumerable themselves.
@Turnerj
Copy link
Member Author

Turnerj commented Sep 25, 2020

What is left is AnyAsync (shouldn't be too bad) and SumAsync (shouldn't be hard, there are just a lot of variations to test due to how the Queryable works).

@JohnCampionJr
Copy link
Contributor

JohnCampionJr commented Sep 25, 2020

Small change suggested, discovered when building DbSet tests for MultiTenant

In QueryableAsyncExtensions.cs, two spots

	//if (source is IMongoFrameworkQueryable)
	if (source.Provider is IMongoFrameworkQueryProvider)

This change allows the extensions to run on the DbSet directly. No tests broke with this change.

@Turnerj
Copy link
Member Author

Turnerj commented Sep 26, 2020

Good idea @JohnCampionJr !

Allows async methods from the DbSet directly
Also fixes the processing extension from Single to SingleOrDefault for Sum/SumAsync
Makes all decimal types save as a real decimal rather than a string value
This helps avoid weird issues in tests while still allowing the core logic to work in normal workloads.
Current version doesn't properly calculate code coverage and the latest version is only available via MyGet

May need to look into better coverage tools that are more reliable as it isn't the first time I've had to work around it.
@Turnerj Turnerj merged commit 1441a83 into master Sep 26, 2020
@Turnerj Turnerj deleted the async-queries branch September 26, 2020 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affects-querying Query-related issue enhancement Enhancements & features moderate-change Moderate changes required
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Async Reading Support
3 participants