Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EF Core TPC Inheritance: InvalidOperationException when using Include on abstract type with navigation properties #35438

Open
samyonr opened this issue Jan 8, 2025 · 0 comments

Comments

@samyonr
Copy link

samyonr commented Jan 8, 2025

I have a TPC hierarchy consisting of an abstract Pet entity (with Dog and Cat as subclasses) and a Person entity. The Person has navigation collections to Dogs and Cats. With these collection navigation properties present, attempting to query Pets and .Include(p => p.Owner) on the abstract type throws:

System.InvalidOperationException: The expression 'p.Owner' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations. For more information on including related data, see https://go.microsoft.com/fwlink/?LinkID=746393.
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression, Boolean setLoaded)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ProcessInclude(NavigationExpansionExpression source, Expression expression, Boolean thenInclude, Boolean setLoaded)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.Expand(Expression query)
   at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query)
   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.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, LambdaExpression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, Expression`1 predicate, CancellationToken cancellationToken)
   at EFCoreTpcAbstractInclude.Tests.QueryFromAbstractTests.IncludeWithoutCasting() in C:\Code\EFCoreTpcAbstractInclude\EFCoreTpcAbstractInclude\EFCoreTpcAbstractInclude.Tests\QueryFromAbstractTests.cs:line 114

If I remove the collection navigations (public ICollection<Dog> Dogs { get; set; } = []; and public ICollection<Cat> Cats { get; set; } = [];) from Person, the query with .Include(p => p.Owner) succeeds without error.

You can see the full example in the following gist, or here: EFCoreTpcAbstractInclude.zip

Here’s the model:

namespace EFCoreTpcAbstractInclude.Entities;

public interface IEntity
{
    public int Id { get; set; }
}

public class Person : IEntity
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int Age { get; set; }

    // Removing these makes the issue go away.
    public ICollection<Dog> Dogs { get; set; } = [];
    public ICollection<Cat> Cats { get; set; } = [];
}

public abstract class Pet : IEntity
{
    public int Id { get; set; }
    public int Age { get; set; }

    public string? Name { get; set; }

    public int OwnerId { get; set; }
    public Person Owner { get; set; } = null!;
}

public class Dog : Pet
{
    public bool LovesChasingSticks { get; set; }
}

public class Cat : Pet
{
    public bool LovesSleeping { get; set; }
}

In the attached examples, I have two tests: IncludeWithCasting that always passes, and IncludeWithoutCasting that fails, but in case the Dogs and Cats collection references are removed from the Person entities (and the Init migration is recreated), it passes. The tests, except for the setup, do:
With casting, always passes

            var bella = await dbContext.Pets
                .Include(p => ((Dog)p).Owner)
                .FirstOrDefaultAsync(p => p.Name == "Bella");

Without casting, fails and throws an exception if a collection reference exists

            var bella = await dbContext.Pets
                .Include(p => p.Owner)
                .FirstOrDefaultAsync(p => p.Name == "Bella");

The migration is the same in both cases, with and without the ICollection references. However, I noticed differences in the generated model snapshots between the two scenarios (with and without the collection navigations on Person). The snapshot with the collections places the foreign key in the derived Dog/Cat entities, while the version without the collections uses a single HasOne("EFCoreTpcAbstractInclude.Entities.Person", "Owner") reference on the Pet entity. This leads me to believe there might be an issue with how EF Core handles TPC, navigation properties, and inheritance in this scenario.

Here's the model snapshot with the collection reference:

// <auto-generated />
using EFCoreTpcAbstractInclude.DataAccess;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EFCoreTpcAbstractInclude.DataAccess.Migrations
{
    [DbContext(typeof(AppDbContext))]
    partial class AppDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasDefaultSchema("AnimalsDb")
                .HasAnnotation("ProductVersion", "9.0.0")
                .HasAnnotation("Relational:MaxIdentifierLength", 63);

            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

            modelBuilder.HasSequence("PersonSequence");

            modelBuilder.HasSequence("PetSequence");

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Person", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("integer")
                        .HasDefaultValueSql("nextval('\"AnimalsDb\".\"PersonSequence\"')");

                    NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));

                    b.Property<int>("Age")
                        .HasColumnType("integer");

                    b.Property<string>("Name")
                        .HasColumnType("text");

                    b.HasKey("Id");

                    b.ToTable("People", "AnimalsDb");

                    b.UseTpcMappingStrategy();
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Pet", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("integer")
                        .HasDefaultValueSql("nextval('\"AnimalsDb\".\"PetSequence\"')");

                    NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));

                    b.Property<int>("Age")
                        .HasColumnType("integer");

                    b.Property<string>("Name")
                        .HasColumnType("text");

                    b.Property<int>("OwnerId")
                        .HasColumnType("integer");

                    b.HasKey("Id");

                    b.ToTable((string)null);

                    b.UseTpcMappingStrategy();
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Cat", b =>
                {
                    b.HasBaseType("EFCoreTpcAbstractInclude.Entities.Pet");

                    b.Property<bool>("LovesSleeping")
                        .HasColumnType("boolean");

                    b.HasIndex("OwnerId");

                    b.ToTable("Cats", "AnimalsDb");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Dog", b =>
                {
                    b.HasBaseType("EFCoreTpcAbstractInclude.Entities.Pet");

                    b.Property<bool>("LovesChasingSticks")
                        .HasColumnType("boolean");

                    b.HasIndex("OwnerId");

                    b.ToTable("Dogs", "AnimalsDb");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Cat", b =>
                {
                    b.HasOne("EFCoreTpcAbstractInclude.Entities.Person", "Owner")
                        .WithMany("Cats")
                        .HasForeignKey("OwnerId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();

                    b.Navigation("Owner");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Dog", b =>
                {
                    b.HasOne("EFCoreTpcAbstractInclude.Entities.Person", "Owner")
                        .WithMany("Dogs")
                        .HasForeignKey("OwnerId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();

                    b.Navigation("Owner");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Person", b =>
                {
                    b.Navigation("Cats");

                    b.Navigation("Dogs");
                });
#pragma warning restore 612, 618
        }
    }
}

And here's the model, but without the collection reference:

// <auto-generated />
using EFCoreTpcAbstractInclude.DataAccess;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace EFCoreTpcAbstractInclude.DataAccess.Migrations
{
    [DbContext(typeof(AppDbContext))]
    partial class AppDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasDefaultSchema("AnimalsDb")
                .HasAnnotation("ProductVersion", "9.0.0")
                .HasAnnotation("Relational:MaxIdentifierLength", 63);

            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

            modelBuilder.HasSequence("PersonSequence");

            modelBuilder.HasSequence("PetSequence");

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Person", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("integer")
                        .HasDefaultValueSql("nextval('\"AnimalsDb\".\"PersonSequence\"')");

                    NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));

                    b.Property<int>("Age")
                        .HasColumnType("integer");

                    b.Property<string>("Name")
                        .HasColumnType("text");

                    b.HasKey("Id");

                    b.ToTable("People", "AnimalsDb");

                    b.UseTpcMappingStrategy();
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Pet", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd()
                        .HasColumnType("integer")
                        .HasDefaultValueSql("nextval('\"AnimalsDb\".\"PetSequence\"')");

                    NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));

                    b.Property<int>("Age")
                        .HasColumnType("integer");

                    b.Property<string>("Name")
                        .HasColumnType("text");

                    b.Property<int>("OwnerId")
                        .HasColumnType("integer");

                    b.HasKey("Id");

                    b.HasIndex("OwnerId");

                    b.ToTable((string)null);

                    b.UseTpcMappingStrategy();
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Cat", b =>
                {
                    b.HasBaseType("EFCoreTpcAbstractInclude.Entities.Pet");

                    b.Property<bool>("LovesSleeping")
                        .HasColumnType("boolean");

                    b.ToTable("Cats", "AnimalsDb");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Dog", b =>
                {
                    b.HasBaseType("EFCoreTpcAbstractInclude.Entities.Pet");

                    b.Property<bool>("LovesChasingSticks")
                        .HasColumnType("boolean");

                    b.ToTable("Dogs", "AnimalsDb");
                });

            modelBuilder.Entity("EFCoreTpcAbstractInclude.Entities.Pet", b =>
                {
                    b.HasOne("EFCoreTpcAbstractInclude.Entities.Person", "Owner")
                        .WithMany()
                        .HasForeignKey("OwnerId")
                        .OnDelete(DeleteBehavior.Cascade)
                        .IsRequired();

                    b.Navigation("Owner");
                });
#pragma warning restore 612, 618
        }
    }
}

I’d expect .Include(p => p.Owner) to work consistently, even if Person has navigation properties to specific Dog/Cat types (the DB looks the same, caused by the exact same migration, so it should be possible to traverse the reference by the Id). Since removing those collections fixes the error, it seems like an EF Core bug (or at least an undocumented limitation) when combining TPC with inheritance-based navigation properties.

I'm using:
Microsoft.EntityFrameworkCore.Design Version="9.0.0"
Microsoft.EntityFrameworkCore.Relational Version="9.0.0"
Npgsql.EntityFrameworkCore.PostgreSQL Version="9.0.2"
Microsoft.EntityFrameworkCore.Abstractions Version="9.0.0"
net9.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants