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

System.InvalidOperationException: The source IQueryable doesn't implement IAsyncEnumerable #40

Open
gojanpaolo opened this issue Apr 1, 2020 · 1 comment

Comments

@gojanpaolo
Copy link

gojanpaolo commented Apr 1, 2020

We're getting an InvalidOperationException when used with ef core and ToListAsync

System.InvalidOperationException: The source IQueryable doesn't implement IAsyncEnumerable<...>. Only sources that implement IAsyncEnumerable can be used for Entity Framework asynchronous operations.
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.AsAsyncEnumerable[TSource](IQueryable`1 source)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at ...(String searchText) in ...
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

e.g.

// .csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.3" />
    <PackageReference Include="NinjaNye.SearchExtensions" Version="3.0.1" />
  </ItemGroup>
</Project>

// Program.cs
using Microsoft.EntityFrameworkCore;
using NinjaNye.SearchExtensions;
using System.Linq;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using var ctx = new Context(new DbContextOptionsBuilder().UseSqlServer(@"server=(localdb)\mssqllocaldb;database=db").Options);
        ctx.Database.EnsureDeleted();
        ctx.Database.EnsureCreated();
        ctx.Foo.Search(f => f.Bar).Containing("").ToList();
        await ctx.Foo.Search(f => f.Bar).Containing("").ToListAsync(); // throws exception
    }
}
public class Context : DbContext
{
    public DbSet<Foo> Foo { get; set; }
    public Context(DbContextOptions options) : base(options) { }
}
public class Foo
{
    public int FooId { get; set; }
    public string Bar { get; set; }
}
@FWest98
Copy link

FWest98 commented Apr 16, 2020

Normal LINQ extension methods return a fresh instance of the IQueryable every time, one that is created using the original Provider. This way, the special properties of the IQueryable that EF creates are preserved after calling various extension methods.

However, for this library to allow you to use the extra functions Containing, etc, only after you called Search first, the Search will not use the provided Provider to build a new IQueryable, instead it will use its own implementation. When calling ToListAsync on that, EF will complain as the EF special properties are lost.

There are two options to mitigate this problem:

  1. Call another normal LINQ method after your search. For example, a Select call will yield an IQueryable from the built-in Provider again, so you can use your Async functions.
  2. Use the following extension methods that provides you with a fresh IQueryable that is provided by EF's Provider:
public static IQueryable<TSource> Apply<TSource, TProperty>(this QueryableSearchBase<TSource, TProperty> source) {
    return source.Where(source.AsExpression());
}

public static IQueryable<TParent> Apply<TParent, TChild, TProperty>(this QueryableChildSearchBase<TParent, TChild, TProperty> source) {
    return source.Where(source.AsExpression());
}

// Usage:
await ctx.Foo.Search(f => f.Bar).Containing("").Apply().ToListAsync();

animha pushed a commit to animha/SearchExtensions that referenced this issue Aug 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants