I'm constructing and executing my queries in a way that's independent of EF-Core, so I'm relying on IQueryable<T>
to obtain the required level of abstraction. I'm replacing awaited SingleAsync()
calls with awaited ToAsyncEnumerable().Single()
calls. I'm also replacing ToListAsync()
calls with ToAsyncEnumerable().ToList()
calls. But I just happened upon the ToAsyncEnumerable()
method so I'm unsure I'm using it correctly or not.
To clarify which extension methods I'm referring to, they're defined as follows:
SingleAsync
andToListAsync
are defined on theEntityFrameworkQueryableExtensions
class in theMicrosoft.EntityFrameworkCore
namespace and assembly.ToAsyncEnumerable
is defined on theAsyncEnumerable
class in theSystem.Linq
namespace in theSystem.Interactive.Async
assembly.
When the query runs against EF-Core, are the calls ToAsyncEnumerable().Single()/ToList()
versus SingleAsync()/ToListAsync()
equivalent in function and performance? If not then how do they differ?
When the original source is a
DbSet
,ToAsyncEnumerable().Single()
is not as performant asSingleAsync()
in the exceptional case where the database contains more than one matching row. But in in the more likely scenario, where you both expect and receive only one row, it's the same. Compare the generated SQL:ToAsyncEnumerable()
breaks theIQueryable
call chain and enters LINQ-to-Objects land. Any downstream filtering occurs in memory. You can mitigate this problem by doing your filtering upstream. So instead of:you can do:
If that approach still leaves you squirmish then, as an alternative, consider defining extension methods like this one:
For methods returning sequence (like
ToListAsync
,ToArrayAsync
) I don't expect a difference.However for single value returning methods (the async versions of
First
,FirstOrDefault
,Single
,Min
,Max
,Sum
etc.) definitely there will be a difference. It's the same as the difference by executing those methods onIQueryable<T>
vsIEnumerable<T>
. In the former case they are processed by database query returning a single value to the client while in the later the whole result set will be returned to the client and processed in memory.So, while in general the idea of abstracting EF Core is good, it will cause performance issues with
IQueryable<T>
because the async processing of queryables is not standartized, and converting toIEnumerable<T>
changes the execution context, hence the implementation of single value returning LINQ methods.P.S. By standardization I mean the following. The synchronous processing of
IQueryable
is provided byIQueryProvider
(standard interface fromSystem.Linq
namespace inSystem.Core.dll
assembly)Execute
methods. Asynchronous processing would require introducing another standard interface similar to EF Core customIAsyncQueryProvider
(insideMicrosoft.EntityFrameworkCore.Query.Internal
namespace inMicrosoft.EntityFrameworkCore.dll
assembly). Which I guess requires cooperation/approval from the BCL team and takes time, that's why they decided to take a custom path for now.According to the official Microsoft documentation for EF Core (all versions, including the current 2.1 one):
Source: https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.query.internal.asynclinqoperatorprovider.toasyncenumerable?view=efcore-2.1
p.s. I personally found it problematic in combination with the AutoMapper tool (at least, until ver. 6.2.2) - it just doesn't map collection of type IAsyncEnumerable (unlike IEnumerable, with which the AutoMapper works seamlessly).
I took a peek at the source code of Single (Line 90).
It cleary illustrates that the enumerator is only advanced once (for a successful operation).
Since this kind of implementation is as good as it gets (nowadays), one can say with certainty that using the Ix Single Operator would not harm performance.
As for SingleAsync, you can be sure that it is implemented in a similar manner, and even if it is not (which is doubtful), it could not outperform the Ix Single operator.