dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.63k stars 3.15k forks source link

Basic `EF.CompileAsyncQuery` Results in `System.ArgumentException`: 'Argument types do not match' #27674

Open Mike-E-angelo opened 2 years ago

Mike-E-angelo commented 2 years ago

File a bug

I appear to be running into a bug with EF.CompileAsyncQuery when returning an instance object/result vs IQueryable (which has worked amazingly well).

When returning an instance object/result, I run into an System.ArgumentException with the simplest of expressions.

Include your code

You should be able to load and hit F5 on this solution here: https://github.com/Mike-E-angelo/Stash/blob/master/EfCore.CompiledQueries.BasicExpression/EfCore.CompiledQueries.BasicExpression.sln

Error is encountered here: https://github.com/Mike-E-angelo/Stash/blob/master/EfCore.CompiledQueries.BasicExpression/EfCore.CompiledQueries.BasicExpression/Worker.cs#L34

var query = EF.CompileAsyncQuery<Context, Statistic>(x => new Statistic
{
    Day = x.Subjects.Count()
});
await query(context); // System.ArgumentException: 'Argument types do not match'

sealed class Context : DbContext
{
    public Context(DbContextOptions options) : base(options) {}
    public DbSet<Subject> Subjects { get; set; } = default!;
}

sealed class Subject
{
    public Guid Id { get; set; }
    public string Name { get; set; } = default!;
}

public class Statistic
{
    public int Day { get; set; }
    public int Week { get; set; }
    public long All { get; set; }
}

Include stack traces

Include the full exception message and stack trace for any exception you encounter.

Use triple-tick fences for stack traces. For example:

System.ArgumentException
  HResult=0x80070057
  Message=Argument types do not match
  Source=System.Linq.Expressions
  StackTrace:
   at System.Linq.Expressions.Expression.Bind(MemberInfo member, Expression expression)
   at System.Linq.Expressions.MemberAssignment.Update(Expression expression)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberAssignment(MemberAssignment node)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
   at System.Linq.Expressions.ExpressionVisitor.Visit[T](ReadOnlyCollection`1 nodes, Func`2 elementVisitor)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberInit(MemberInitExpression node)
   at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CreateCompiledAsyncQuery[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledAsyncTaskQuery`2.CreateCompiledQuery(IQueryCompiler queryCompiler, Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryBase`2.<>c.<EnsureExecutor>b__6_0(CompiledQueryBase`2 t, TContext c, LambdaExpression q)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam1,TParam2,TParam3,TValue](TValue& target, TParam1 param1, TParam2 param2, TParam3 param3, Func`4 valueFactory)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryBase`2.EnsureExecutor(TContext context)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryBase`2.ExecuteCore(TContext context, CancellationToken cancellationToken, Object[] parameters)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryBase`2.ExecuteCore(TContext context, Object[] parameters)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledAsyncTaskQuery`2.ExecuteAsync(TContext context)
   at EfCore.CompiledQueries.BasicExpression.Worker.<StartAsync>d__3.MoveNext() in ...\Mike-E-angelo\Stash\EfCore.CompiledQueries.BasicExpression\EfCore.CompiledQueries.BasicExpression\Worker.cs:line 34
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at EfCore.CompiledQueries.BasicExpression.Worker.<StartAsync>d__3.MoveNext() in ...\Mike-E-angelo\Stash\EfCore.CompiledQueries.BasicExpression\EfCore.CompiledQueries.BasicExpression\Worker.cs:line 37
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>d__12.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.<RunAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.<RunAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at EfCore.CompiledQueries.BasicExpression.Program.<Main>(String[] args)

  This exception was originally thrown at this call stack:
    [External Code]
    EfCore.CompiledQueries.BasicExpression.Worker.StartAsync(System.Threading.CancellationToken) in Worker.cs
    [External Code]
    EfCore.CompiledQueries.BasicExpression.Worker.StartAsync(System.Threading.CancellationToken) in Worker.cs
    [External Code]

Include verbose output

NA

Include provider and version information

EF Core version: 6.0.3 Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer) Microsoft.EntityFrameworkCore.SqlServer Target framework: (e.g. .NET 5.0) net6.0 Operating system: Windows 10 IDE: (e.g. Visual Studio 2019 16.3) Visual Studio 2022 17.1 RTM

smitpatel commented 2 years ago

Work-around - Can use sync version.

Blocked on #14551

Issue: While user is using async API that means we should run query in async way, the queryable operators inside (Count) is sync version. So when we insert query in place of that Count, it will be in async version returning Task<int> hence the types don't match. There are 2 ways to avoid

May be we need to throw error using single result operation from sync API inside async compiled query.

Mike-E-angelo commented 2 years ago

Thank you for the investigation @smitpatel. I appreciate you taking the time to look into this.

I hate to ask, but how does the IQueryable versions manage to work? They are ultimately replacing/running async versions of the synchronous expressions, correct?

Running a sync version of the operation is not a viable workaround, I am afraid. That as well as .Result will lead to thread starvation and/or deadlock if I understand correctly.

The workaround that I am using the IQueryable version and calling SingleOrDefaultAsync on that. I am assuming of course that everything is asynchronous evaluated, and am hoping I am correct in that assumption. :S

smitpatel commented 2 years ago

The difference is not the operation but rather where the operation occurs. When you are using IQueryable version then you return the enumerable generated by EF Core as return of the compiled query delegate. In this case you are assigning result value to Statistic member. If you put the Count operation result as the return of the compiled query delegate that will also work.

Mike-E-angelo commented 2 years ago

If you put the Count operation result as the return of the compiled query delegate that will also work.

I did consider doing this but the reason I am using the projection Statistic class is I wanted to get 3 counts with one query and store the results in the projection. My understanding is that if the expression has 3 counts, it pulls it as one query and puts the results into the projection class. Whereas if I return just the count as the result value as you are describing, I would need to make 3 separate expressions which each have to be compiled on their own and call the database, resulting in 3 total queries to the database instead of one.

That is my understanding. I would appreciate knowing if I am overlooking and/or misunderstanding something somewhere.

AndriySvyryd commented 2 years ago

Another possible workaround is to use query batching when it's implemented

Mike-E-angelo commented 2 years ago

Great, thank you for the suggestion @AndriySvyryd. My primary concern is to ensure that I understand everything as designed as it still is challenging to know what is taking place on the (sql) server vs client. What I have been using to gauge the location are the generated SQL scripts that are executed by EFCore via logging. For instance, in the above if there are 3 generated queries/calls then that seems off, and I would see if I can make it one.

It looks like the issue you mention has been open since 2018 which, difficult to believe, is nearly half a decade ago already. šŸ˜¬ It hasn't even been slated for a milestone if I understand correctly so that does not look like a viable workaround in the short (or even medium) term.

All things considered, I am happy with my current workaround of using the IQueryable compilation route and calling a Take(1)/SingleOrDefaultAsync on it.

Along those lines, I'd like to take this opportunity to thank everyone over there for all your diligent, impressive, and amazing work with this project. Despite these minor challenges, I have been able to query my entire domain model with some pretty involved acrobatics and not have to write any actual SQL to manage/maintain.

For further illustration, my Blazor server-side solution is currently sitting at 106,000 lines of C# code, with zero SQL and only ~100 lines of custom JavaScript. To me, this is living the .NET/C# dream and it's due in part to your project here.

FWIW I have a shout out to the team there in my acknowledgements here: https://alpha.starbeam.one/about/acknowledgements

Thank you again for all your notable efforts and accomplishments. šŸ™

joakimriedel commented 1 year ago

@Mike-E-angelo I've yet to use CompileAsyncQuery but to prevent EF Core from running multiple SQL queries to the server I had to put a dummy .Where(x => true) before .Count() see this issue: https://github.com/dotnet/efcore/issues/27728 If this is not at all related, feel free to hide this comment.