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.71k stars 3.17k forks source link

Concat different derived entities using OfType (TPH) throws exception #27695

Open joakimriedel opened 2 years ago

joakimriedel commented 2 years ago

I would expect the following code to work, but it throws an exception.

See the following gist for full repro.

Expression of type 'System.Linq.IQueryable`1[EfCoreBug.Program+Cat]' cannot be used for parameter of type 'System.Linq.IQueryable`1[EfCoreBug.Program+Dog]' 
                    of method 'System.Linq.IQueryable`1[EfCoreBug.Program+Dog] Concat[Dog](System.Linq.IQueryable`1[EfCoreBug.Program+Dog], System.Collections.Generic.IEnumerable`1[EfCoreBug.Program+Dog])'
                var userCatsAndAllDogs = await context.Users
                    .Where(u => u.Id == 1)
                    .SelectMany(m => m.Animals.OfType<Cat>().Cast<Animal>())
                    .Concat(
                        context.Animals.OfType<Dog>().Cast<Animal>()
                    )
                    .ToListAsync();

Also tried the following, but it does client eval and throws

Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
                var userCatsAndAllDogs = await context.Users
                    .Where(u => u.Id == 1)
                    .SelectMany(m => m.Animals.OfType<Cat>().Select(c => c as Animal))
                    .Concat(
                        context.Animals.OfType<Dog>().Select(d => d as Animal)
                    )
                    .ToListAsync();

This is a workaround, but I'd prefer using OfType if possible somehow?

                var userCatsAndAllDogs = await context.Users
                    .Where(u => u.Id == 1)
                    .SelectMany(m => m.Animals.Where(a => EF.Property<string>(a, "Discriminator") == "Cat"))
                    .Concat(
                        context.Animals.Where(a => EF.Property<string>(a, "Discriminator") == "Dog")
                    )
                    .ToListAsync();

The resulting SQL query from workaround is the following, which I would have expected also from using OfType.

      SELECT [t].[Id], [t].[Discriminator], [t].[UserId], [t].[NumberOfMiceKilled], [t].[IsShoeChewer]
      FROM [Users] AS [u]
      INNER JOIN (
          SELECT [a].[Id], [a].[Discriminator], [a].[UserId], [a].[NumberOfMiceKilled], [a].[IsShoeChewer]
          FROM [Animals] AS [a]
          WHERE [a].[Discriminator] = N'Cat'
      ) AS [t] ON [u].[Id] = [t].[UserId]
      WHERE [u].[Id] = 1
      UNION ALL
      SELECT [a0].[Id], [a0].[Discriminator], [a0].[UserId], [a0].[NumberOfMiceKilled], [a0].[IsShoeChewer]
      FROM [Animals] AS [a0]
      WHERE [a0].[Discriminator] = N'Dog'

Include stack traces

An exception of type 'System.ArgumentException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Expression of type 'System.Linq.IQueryable`1[EfCoreBug.Program+Cat]' cannot be used for parameter of type 'System.Linq.IQueryable`1[EfCoreBug.Program+Dog]' of method 'System.Linq.IQueryable`1[EfCoreBug.Program+Dog] Concat[Dog](System.Linq.IQueryable`1[EfCoreBug.Program+Dog], System.Collections.Generic.IEnumerable`1[EfCoreBug.Program+Dog])''
   at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
   at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression arg0, Expression arg1)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ProcessSetOperation(NavigationExpansionExpression outerSource, MethodInfo genericMethod, NavigationExpansionExpression innerSource)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.Expand(Expression query)
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query)
   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.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__65`1.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`1.GetResult()
   at EfCoreBug.Program.<Main>d__1.MoveNext()

Include provider and version information

EF Core version: 6.0.3 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .net 6 Operating system: Win11 IDE: VS Code

smitpatel commented 2 years ago

Blocked on https://github.com/dotnet/efcore/issues/16298

For fix of this particular issue in Nav expansion, pass in the set operation type to the method. Currently we infer type from outer or inner based on what is assignable to what. Though for mismatching types, that is incorrect. In above query we remove Cast to Animal since it is converting to base type and redundant (except for set operation case). We need to preserve that set operation is on Animal rather than Cat/Dog.