dotnet / dotnet/efcore

ComplexCollection Entity splitting support

Open
#37,982 3 comments 0 reactions 1 assignee Claimed by @AndriySvyryd View on GitHub
area-complex-types area-model-building customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

### Bug description

When using a ComplexCollection (JSON) mapping on an entity that is split across multiple tables using `SplitToTable`, queries that include the complex collection fail with:

`System.InvalidOperationException: Sequence contains more than one element`

Additional issues observed:
- Despite using Table splitting, the framework attempts to write data for the "Tags" collection to both tables
- The `[Column("tags")]` attribute is ignored and the C# property name "Tags" is used instead for column mapping

### Your code

```csharp
#:package Microsoft.EntityFrameworkCore@10.0.5
#:package Microsoft.EntityFrameworkCore.Sqlite@10.0.5
#:property PublishAot=false

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations.Schema;

await using var conn = new SqliteConnection("Data Source=:memory:");
conn.Open();

var options = new DbContextOptionsBuilder()
.UseSqlite(conn)
.EnableDetailedErrors()
.LogTo(Console.WriteLine, LogLevel.Information)
.Options;

await using (var ctx = new AppDbContext(options))
{
ctx.Database.EnsureCreated();

var post = new Post
{
Title = "Hello",
IsFeatured = true,
Tags =
[
new Tag { Name = "a" },
new Tag { Name = "b" }
]
};
ctx.Posts.Add(post);
await ctx.SaveChangesAsync();
}

await using (var ctx = new AppDbContext(options))
{
var data = await ctx.Posts
.AsNoTracking()
.Where(p => p.Tags.Any(t => t.Name.ToLower().Contains("a")))
.ToListAsync();

Console.WriteLine($"Posts count: {data.Count}");
}

[Table("posts")]
class Post
{
[Column("id")]
public int Id { get; set; }

[Column("title")]
public string? Title { get; set; }

[Column("is_featured")]
public bool IsFeatured { get; set; }

[Column("tags")]
public List Tags { get; set; } = [];
}

class Tag
{
public string Name { get; set; } = string.Empty;
}

class AppDbContext(DbContextOptions options) : DbContext(options)
{
public DbSet Posts => Set();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasKey(p => p.Id);
modelBuilder.Entity().ComplexCollection(p => p.Tags, c => c.ToJson());

modelBuilder.Entity().SplitToTable("posts_extra", tb =>
{
tb.Property(p => p.IsFeatured);
});
}
}
```

### Stack traces

```text
Unhandled exception. 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.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.GenerateJsonReader(Int32 jsonColumnIndex, ITypeBase structuralType)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessTopLevelComplexJsonProperties(StructuralTypeShaperExpression shaper, ParameterExpression instanceVariable, List`1 expressions)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.AddStructuralTypeInitialization(StructuralTypeShaperExpression shaper, ParameterExpression instanceVariable, List`1 variables, List`1 expressions)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.StructuralTypeMaterializerInjector.MaterializeEntity(StructuralTypeShaperExpression shaper, ParameterExpression materializationContextVariable, ParameterExpression concreteEntityTypeVariable, ParameterExpression instanceVariable, ParameterExpression entryVariable)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.StructuralTypeMaterializerInjector.ProcessStructuralTypeShaper(StructuralTypeShaperExpression shaper)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.StructuralTypeMaterializerInjector.VisitExtension(Expression extensionExpression)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.StructuralTypeMaterializerInjector.Inject(Expression expression)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.InjectStructuralTypeMaterializers(Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.VisitExtension(Expression extensionExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper(Expression shaperExpression, Expression& relationalCommandResolver, IReadOnlyList`1& readerColumns, LambdaExpression& relatedDataLoaders, Int32& collectionId)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitShapedQuery(ShapedQueryExpression shapedQueryExpression)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)
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.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)
at Program.$(String[] args)
at Program.$(String[] args)
at Program.$(String[] args)
at Program.(String[] args)
```

### Verbose output

```text

```

### EF Core version

10.0.5

### Database provider

Npgsql.EntityFrameworkCore.PostgreSQL, Microsoft.EntityFrameworkCore.Sqlite

### Target framework

.NET 10.0

### Operating system

_No response_

### IDE

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.