ninjanye / SearchExtensions

Library of IQueryable extension methods to perform searching
MIT License
331 stars 52 forks source link

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

Open gojanpaolo opened 4 years ago

gojanpaolo commented 4 years ago

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 commented 4 years ago

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