Skip to content
New issue

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

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

Already on GitHub? # to your account

Allow complex types in base types in TPC and unmapped base types in TPT #35025

Open
dvdwouwe opened this issue Oct 31, 2024 · 7 comments
Open

Comments

@dvdwouwe
Copy link

Include your code

The combination of inheritance mapping and the use of Complex types, gives strange behavior in TPC mapping.
I tried TPH and TPT (line 118), and there everything works as expected.

What I see is:

  • model seems ok, but the creation of tables is not ok -> columns of the complex type are missing
  • If I run polymorphic queries I get an exception in TPC, works fine in TPH and TPT

See https://github.com/dvdwouwe/danny-playground-ef/tree/main for reproducing the behavior we see.
Commit SHA: d194a6f9a7a783a5579a03ed9ceb764955e0dd2d

Include provider and version information

EF Core version: 8.0.10
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0.403
Operating system: Windows 11
IDE: Rider

@cincuranet
Copy link
Contributor

cincuranet commented Oct 31, 2024

Not Microsoft.EntityFrameworkCore.SqlServer specific, repros on Microsoft.EntityFrameworkCore.Sqlite as well. Repros on 9.0.0-rc.2.24474.1.

Smaller repro
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
await context
    .Set<RealEvent>()
    .Where(e => e.Knowledge.To == null)
    .ToListAsync();

public abstract class EventBase
{
    protected EventBase(int id)
    {
        Id = id;
    }

    public int Id { get; set; }
    public Period Knowledge { get; set; }
}

public class RealEvent : EventBase
{
    public RealEvent(int id) : base(id)
    { }
}

public class Period
{
    public DateTimeOffset From { get; set; }
    public DateTimeOffset? To { get; set; }
}

public class MyDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .EnableDetailedErrors()
            .EnableSensitiveDataLogging()
            .UseSqlite()
            .LogTo(Console.WriteLine, LogLevel.Information);

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<EventBase>(builder =>
        {
            builder.ComplexProperty(e => e.Knowledge);
            builder.UseTpcMappingStrategy();
        });
        modelBuilder.Entity<RealEvent>();
    }
}

The table created is only:

      CREATE TABLE "RealEvent" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RealEvent" PRIMARY KEY
      );

Also, query throws exception:

   at System.Linq.ThrowHelper.ThrowNoElementsException()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression.GenerateComplexPropertyShaperExpression(StructuralTypeProjectionExpression containerProjection, IComplexProperty complexProperty)
   at Microsoft.EntityFrameworkCore.Query.StructuralTypeProjectionExpression.BindComplexProperty(IComplexProperty complexProperty)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.BindComplexProperty(StructuralTypeReferenceExpression typeReference, IComplexProperty complexProperty)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryBindMember(Expression source, MemberIdentity member, Expression& expression, IPropertyBase& property)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryBindMember(Expression source, MemberIdentity member, Expression& expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMember(MemberExpression memberExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMember(MemberExpression memberExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Sqlite.Query.Internal.SqliteSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateExpression(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateLambdaExpression(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   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.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[TSource](IQueryable`1 source, CancellationToken cancellationToken)

@maumar
Copy link
Contributor

maumar commented Nov 7, 2024

When trying to bind complex property we are looking for table mapping so that we know which table/columns to bind. However the table mapping is missing here - We build those as part of RelationalModel.Create. Here, the complex type is defined on the abstract base type, and we skip those when creating table mappings. When processing the derived type we only look at declared complex types and so we miss the type defined on the base. As a result the complex property ends up with no table mapping

@maumar maumar removed their assignment Nov 7, 2024
@dvdwouwe
Copy link
Author

Hi,

Actually this is blocking us. We have a rather huge (potential) project by a large company, but we need an example that is production ready. The expectations for this in August 2025. If it is successful, it will be one of our biggest projects ever.

TPH mapping is not very suitable for this:

  • too many null columns in this case
  • complex unique constraints, always take into account the discriminator field
  • too complex indices
  • if db admins see such a huge table, they really don't like it

TPT is too slow, because of the huge number of abstract subclasses.

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Dec 11, 2024

@dvdwouwe There are two workarounds that you can consider using for now:

  1. Use owned types instead of complex types for this case. This means that they will get a shadow PK, so you might need to do extra work when you are attaching entities containing them
  2. Ignore the complex properties on the base type and only configure it on the leaf types. This would mean that you can't use these properties in queries at the base type level:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<EventBase>(builder =>
    {
        builder.Ignore(e => e.Knowledge);
        builder.UseTpcMappingStrategy();
    });
    modelBuilder.Entity<EventWithName>(builder =>
    {
        builder.ComplexProperty(e => e.Knowledge);
    });
    modelBuilder.Entity<EventWithPartnerType1>(builder =>
    {
        builder.ComplexProperty(e => e.Knowledge);
    });
    modelBuilder.Entity<EventWithPartnerType2>(builder =>
    {
        builder.ComplexProperty(e => e.Knowledge);
    });
}

@dvdwouwe
Copy link
Author

@AndriySvyryd ,

Thanks for the suggestions.

I have already experimented with both approaches:

  1. Owned types: It seems they are only available on leaf nodes, even if they are defined on a non-leaf type.
  2. Complex properties: These are also limited to leaf nodes.

Both workarounds present the same significant issue:

  • I cannot use polymorphic queries. I have many subclasses and need to query a tree of classes. In both cases, the workaround doesn't solve this problem.

The only solution (workaround) that works for me is using TPH (Table Per Hierarchy). With TPH, the complex properties are recognized correctly, and I can use polymorphic queries.

@AndriySvyryd
Copy link
Member

Owned types: It seems they are only available on leaf nodes, even if they are defined on a non-leaf type.

You are right, for owned types this is tracked by #32028

@AndriySvyryd AndriySvyryd changed the title ComplexType properties disappear when using TPC mapping Allow complex types in base types in TPC and unmapped base types in TPT Dec 12, 2024
@AndriySvyryd AndriySvyryd added this to the 10.0.0 milestone Dec 12, 2024
@roji
Copy link
Member

roji commented Dec 30, 2024

@cincuranet for the query part, assigning to you as a complex type query issue; I recommend looking into this together with #35392.

@roji roji assigned cincuranet and unassigned roji Dec 30, 2024
@roji roji added the type-bug label Dec 30, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment