Closed hez2010 closed 2 years ago
It is by design. A true async query provider will never have issues with method signatures we have since we have implemented one. So correct way to solve this would be to implement truly async code path.
If you want to fake async by calling into sync code path then you may use reflection to see type of TResult and modify it and call appropriate method accordingly. If that does not work or you run into other issues then I suggest asking question on stackoverflow since it is not our intention to facilitate having sync code path substitute for async.
Why is this considered "by design"? According to Microsoft's documentation about asynchronous methods they should return Task, Task
Because the TResult
itself is Task<AnotherType>
or IAsyncEnumerable<AnotherType>
. If you call into this method then pass in correct generic type. It works correctly in EF Core implementation which runs whole variety of query asynchronously so if you are running into issue then it is likely bug in your code.
Thanks for the prompt response. I'm still not clear on how that is expected to work. The implementation in question is like the OP, to facilitate a substitute proxy for an IQueryable for testing purposes. The implementation of IAsyncQueryProvider wraps an inner IQueryProvider (I.e. an IQueryable<T> _inner fed a List<T>) where:
public TResult Execute<TResult>(Expression expression)
{
return _inner.Execute<TResult>(expression);
}
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// ??
// With IDbAsyncQueryProvider as Task<TResult> this was...
// return Task.FromResult(Execute<TResult>(expression));
}
I don't quite grasp how in the case of the synchronous Execute method TResult represents a result of the expression executing, but the Asynchronous Execute method TResult would somehow be expected/interpreted as a Task<TSomeOtherResult>? Then there is the question around how this deviates from MS' own documentation around what asyncrhonous methods should be expected to return. How would an ExecuteAsync participate in a WaitAll() type scenario? (Not required in my case, but still begs the question how such an asynchronous implementation is expected to play nice with other Tasks)
The reason for deviation is variation in the result type and the way it can be used.
An Execute can return a TResult
when it is single result kind of query or IEnumerable<T>
when it is enumerable kind of query. Both of them are represented as TResult in the signature.
Wrapping inside task for ExecuteAsync
doesn't work because the result type is not Task<T>
and Task<IEnumerable<T>>
which is significantly different from any other combination of sync/async method. The results are going to be Task<T>
and IAsyncEnumerable<T>
. The symmetry argument from other methods doesn't hold here. Further the result types are not invalid either. A single result returning Task<T>
can be awaited to get result (symmetry in use case with sync path), whereas the result of IAsyncEnumerable<T>
can be used with await foreach
(again holding symmetry with sync consumer pattern).
So the return types, signature, expected usage everything is in order here. As for the doc you are showing it does say that the method can return either of the Task<T>
and IAsyncEnumerable<T>
The only way to represent that in code is to have method return TResult and TResult can be either of those types. (which a well constructed expression tree represents just fine). So we are aligned with documentation also in a sense.
Now the question of OP and you both, simple answer is you need to implement an async code path. The "hack" of converting a TResult
to Task<TResult>
using Task.FromResult
method only works for single result scenario. You cannot directly convert an IEnumerable<T>
to IAsyncEnumerable<T>
. So please write your own code which performs the conversion as required.
Beyond that, the original ask in the title & how to implement IAsyncQueryProvider
has been explained already. If you want to use certain implementation and it is not working then I reckon that is outside of the scope of EF Core and may be more suitable to ask on question-answer forums online.
Thanks for that explanation, I see now where that TResult was a bit misleading and how/why the QueryProvider would be written to work with either a Task<T> or IAsyncEnumerable<T> (guessing previous generations was Task<IEnumerable<T>>)
In any case I did a bit of digging around for recent implementations for IAsyncQueryProvider and came across MockQueryable by Roman Titov which includes a Moq implementation as well which looks like it will fill in nicely. I also had a dig into the source out of curiosity in terms of how that async override should be handled so I'll be digesting that, especially around how IAsyncEnumerable might come into play.
IAsynQueryProvider.ExecuteAsync
returnsTResult
but notTask<TRsult>
orIAsyncEnumerable<TResult>
.I want to create a class implements
IAsyncQueryProvider
, and it requires to implementpublic TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
.However, when I use my TestProvider, if I invoking
xx.FirstOrDefaultAsync()
, it will passTask<TEntity>
as type parameterTResult
, and also theIQueryable<T>.Provider.Execute
will return aTEntity
, which will lead to a cast exception:TEntity
cannot convert toTask<TEntity>
.Is this a design failure in IAsyncQueryProvider? Please change
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
topublic Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
orpublic IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
. Otherwise please tell me how can I do to solve this problem? Thanks!For detailed code, see: https://github.com/VahidN/EFSecondLevelCache.Core/blob/7a2d5645e68a079385abb9b3b0c8fb9f48947880/src/EFSecondLevelCache.Core/EFCachedQueryProvider.cs#L142
Further technical details
EF Core version: 3.0.0-preview7.19362.6