efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Allow parameterized types in queries using inheritance mapping (TPH, possibly even TPC/TPT)

Open jbhelm opened this issue 1 year ago • 3 comments

Using a type variable in a Where() predicate to compare to GetType() fails to translate; only a hardcoded type using typeof(...) seems to be supported. Tested on both InMemory and SqlServer databases.

Repro

using Microsoft.EntityFrameworkCore;

namespace EfCoreExample {

    internal class GetTypeExample {

        public class Model {
            public int Id { get; set; }
        }


        public class DataContext : DbContext {
            public DbSet<Model> Models { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder options)
                => options.UseInMemoryDatabase("example");
            //options.UseSqlServer(connectionString: @"Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=EfCoreExample;Integrated Security=True;MultipleActiveResultSets=true;Trusted_Connection=True;");
        }


        public static void Run() {
            using var db = new DataContext();
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();
            
            _ = db.Models
                .Where(x => x.GetType() == typeof(Model))  // <-- works
                .ToArray();

            var type = typeof(Model);
            _ = db.Models
                .Where(x => x.GetType() == type)    // <-- fails with "The LINQ expression 'DbSet<Model>().Where(m => m.GetType() == __type_0)' could not be translated"
                .ToArray();
        }
    }

}

Results in

System.InvalidOperationException
  HResult=0x80131509
  Message=The LINQ expression 'DbSet<Model>()
    .Where(m => m.GetType() == __type_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorExpression[TResult](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__DisplayClass11_0`1.<ExecuteCore>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
   at EfCoreExample.GetTypeExample.Run() in C:\Users\jhelm\dev\EfCoreExample\GetTypeExample.cs:line 31
   at EfCoreExample.Program.Main(String[] args) in C:\Users\jhelm\dev\EfCoreExample\Program.cs:line 7

EF Core version: 8.0.11, 9.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.InMemory Target framework: .NET 8.0 Operating system: Microsoft Windows [Version 10.0.22635.4515] IDE: Visual Studio 2022 17.11.6

jbhelm avatar Dec 05 '24 16:12 jbhelm

That's right, this isn't supported. Consider what it means to allow for parameterizing a type check when translating a query to SQL... If you're using TPT or TPC, each .NET type is mapped to a different table, meaning that the table name must vary from query to query based on that parameter. But SQL does not allow parameterizing table names (or any part of the query schema), since that makes the query completely dynamic. So EF would have to produce different SQLs for different parameter values; this is something we generally avoid doing unless it's very needed (e.g. inlining array elements for Contains and similar), for various reasons.

For TPH things are somewhat better: EF could derive the discriminator value from the type parameter and send that as a parameter to the database. But this isn't something anybody has asked for before, as far as I'm aware.

I'll put this in the backlog so that other people can vote for it.

roji avatar Dec 05 '24 17:12 roji

Would it work with the new EF.Constant ?

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.ef.constant?view=efcore-9.0

PhenX avatar Dec 10 '24 08:12 PhenX

No, for the same reasons explained above.

roji avatar Dec 10 '24 14:12 roji