efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Exception thrown with split tables: System.InvalidOperationException: 'Sequence contains more than one element'

Open cobrul opened this issue 1 year ago • 4 comments

With EF Core 8.0.7 and 9.0.0, if I have an entity split to more than one table I get an error when trying to check for null. The error appears in RelationalSqlTranslatingExpressionVisitor method TryRewriteEntityEquality, due to a call to SingleOrDefault().

Code to reproduce (I am using SqlServer but it appears also with Sqlite):

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EfCoreBug1Demo;

class Program
{
    static void Main()
    {
        var connection = new SqliteConnection("Data Source=:memory:");
        connection.Open();

        var options = new DbContextOptionsBuilder<TestContext>()
            .UseSqlite(connection)
            .Options;

        using (var dbContext = new TestContext(options))
        {
            dbContext.Database.EnsureDeleted();
            dbContext.Database.EnsureCreated();
        }

        using (var dbContext = new TestContext(options))
        {
            var query = dbContext.Set<InvoiceLine>().AsNoTracking()
                        .Select(x => new
                        {
                            x.Id,
                           // throws exception
                           Name = x.Invoice.Creator != null ? x.Invoice.Creator!.FullName : "",
                           // works
                           //Name = x.Invoice.CreatorId != null ? x.Invoice.Creator!.FullName : ""
                        });

            var result = query.ToArray();
        }
    }
}

internal class TestContext : DbContext
{
    public TestContext(DbContextOptions options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var assembly = typeof(Program).Assembly;
        modelBuilder.ApplyConfigurationsFromAssembly(assembly);
    }
}

internal class Person
{
    public int Id { get; set; }
    public string FullName { get; set; } = "";
    public byte[] Photo { get; set; } = [];
    public int PhotoVersion { get; set; } = 1;
}

internal class Invoice
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public required Person Customer { get; set; }
    public int? CreatorId { get; set; }
    public Person? Creator { get; set; }
}

internal class InvoiceLine
{
    public int Id { get; set; }
    public int InvoiceId { get; set; }
    public required Invoice Invoice { get; set; }
}


internal class InvoiceConfig: IEntityTypeConfiguration<Invoice>
{
    public void Configure(EntityTypeBuilder<Invoice> builder)
    {
        builder.HasOne(e => e.Customer).WithMany().HasForeignKey(e => e.CustomerId).OnDelete(DeleteBehavior.Restrict);
        builder.HasOne(e => e.Creator).WithMany().HasForeignKey(e => e.CreatorId).OnDelete(DeleteBehavior.SetNull);
        builder.HasKey(e => e.Id);
        builder.ToTable("Invoices");
    }
}

internal class InvoiceLineConfig : IEntityTypeConfiguration<InvoiceLine>
{
    public void Configure(EntityTypeBuilder<InvoiceLine> builder)
    {
        builder.HasOne(e => e.Invoice).WithMany().HasForeignKey(e => e.InvoiceId).OnDelete(DeleteBehavior.Cascade);
        builder.HasKey(e => e.Id);
        builder.ToTable("InvoiceLines");
    }
}

internal class PersonConfig : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {     
        builder.Property(e => e.FullName).IsRequired().HasMaxLength(132);

        builder.ToTable("Persons")
            .SplitToTable("PersonsPhotos", t =>
            {
                t.Property(e => e.Id).HasColumnName("PersonId");
                t.Property(e => e.Photo);
                t.Property(e => e.PhotoVersion);
            });

        builder.HasKey(e => e.Id);
        builder.ToTable("Persons");  
    }    
}

Stack trace:

System.InvalidOperationException: Sequence contains more than one element
   at System.Linq.ThrowHelper.ThrowMoreThanOneElementException()
   at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.<>c__DisplayClass57_0.<TryRewriteStructuralTypeEquality>g__TryRewriteEntityEquality|0(Expression& result)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryRewriteStructuralTypeEquality(ExpressionType nodeType, Expression left, Expression right, Boolean equalsMethod, Expression& result)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Sqlite.Query.Internal.SqliteSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitConditional(ConditionalExpression conditionalExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateProjection(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitNew(NewExpression newExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
   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.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 EfCoreBug1Demo.Program.Main() in d:\EfCoreBug1Demo\Program.cs:line 33

Image

EF Core version: 8.0.7 / 9.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer / Microsoft.EntityFrameworkCore.Sqlite Target framework: (e.g. .NET 8.0) Operating system: Windows 10 IDE: Visual Studio 2022 17.11.4

cobrul avatar Dec 08 '24 08:12 cobrul

Guys, are there any updates and timelines for this problem?

NikitaReut7 avatar Dec 28 '24 21:12 NikitaReut7

@NikitaReut7 it can take us some time to get around to investigating a problem - especially during the December holiday season. You'll have to be patient for a bit more.

roji avatar Dec 29 '24 09:12 roji

@roji I see, thanks

NikitaReut7 avatar Dec 29 '24 13:12 NikitaReut7

I can reproduce this on main. Problem is that when we try to see if we are comparing entity with optional dependent, we assume the entity will always be mapped to one table (outside TPC which we filter out).

maumar avatar Jan 14 '25 08:01 maumar

I also seems to encounter this issue. Table splitting seemed too good and easy until I got this on a query further down my application.

Until the task to review Single in the pipeline is completed in NET11, what is the recommendation to watch out for to avoid hitting this? Not to use SplitToTable() at all? Avoid null checking?

Dunge avatar Nov 04 '25 00:11 Dunge