ninjanye / SearchExtensions

Library of IQueryable extension methods to perform searching

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

gojanpaolo opened this issue · comments

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; }
}

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();