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
I have a TPC hierarchy consisting of an abstract
Petentity (withDogandCatas subclasses) and aPersonentity. ThePersonhas navigation collections toDogsandCats. With these collection navigation properties present, attempting to queryPetsand.Include(p => p.Owner)on the abstract type throws:If I remove the collection navigations (
public ICollection<Dog> Dogs { get; set; } = [];andpublic ICollection<Cat> Cats { get; set; } = [];) fromPerson, 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:
In the attached examples, I have two tests:
IncludeWithCastingthat always passes, andIncludeWithoutCastingthat fails, but in case theDogsandCatscollection references are removed from thePersonentities (and theInitmigration is recreated), it passes. The tests, except for the setup, do:With casting, always passes
Without casting, fails and throws an exception if a collection reference exists
The migration is the same in both cases, with and without the
ICollectionreferences. However, I noticed differences in the generated model snapshots between the two scenarios (with and without the collection navigations onPerson). The snapshot with the collections places the foreign key in the derivedDog/Catentities, while the version without the collections uses a singleHasOne("EFCoreTpcAbstractInclude.Entities.Person", "Owner")reference on thePetentity. 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:
And here's the model, but without the collection reference:
I’d expect
.Include(p => p.Owner)to work consistently, even ifPersonhas navigation properties to specificDog/Cattypes (the DB looks the same, caused by the exact same migration, so it should be possible to traverse the reference by theId). 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