dotnet / dotnet/efcore

Union/Concat returning more than one entity with owned types generates incorrect joins

Open
#33,539 2 comments 0 reactions 0 assignees View on GitHub
area-owned-entities area-query customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

While performing set operation on two queries that both return two entities and the second entity has an owned type, joins for owned type's properties are made to the first table causing them to be empty.

```C#
var studentIds = new List { 123 };
var planningsOnDate = c.Plannings.Where(p => p.EventsDate == new DateTime(2024, 04, 04));

var query = planningsOnDate.SelectMany(p =>
p.AutoAllocations.Where(a => studentIds.Contains(a.Student.Id)),
(p, a) => new { a.Student, studentPlanning = p }
)
.Union(
planningsOnDate.SelectMany(p =>
p.Allocations.Where(a => studentIds.Contains(a.Student.Id)),
(p, a) => new { a.Student, studentPlanning = p })
);

var results = await query.ToListAsync();

Console.WriteLine(results.First().studentPlanning.TimeRange); // null
```
In the above query `Planning` entity has an owned type `TimeRange` with `StartTime` and `EndTime` properties
generated query:
```sql
exec sp_executesql N'SELECT [t0].[Id], [t0].[Id0], [t0].[EventsDate], [e7].[Id], [e7].[EndTime], [e7].[StartTime]
FROM (
SELECT [e2].[Id], [e].[Id] AS [Id0], [e].[EventsDate]
FROM [Planning] AS [e]
INNER JOIN (
SELECT [e0].[Id], [e0].[StudentId], [e0].[PlanningId], [e1].[Id] AS [Id0]
FROM [AutoAllocation] AS [e0]
INNER JOIN [Student] AS [e1] ON [e0].[StudentId] = [e1].[Id]
WHERE [e1].[Id] IN (
SELECT [s].[value]
FROM OPENJSON(@__studentIds_0) WITH ([value] int ''$'') AS [s]
)
) AS [t] ON [e].[Id] = [t].[PlanningId]
INNER JOIN [Student] AS [e2] ON [t].[StudentId] = [e2].[Id]
WHERE [e].[EventsDate] = ''2024-04-04''
UNION
SELECT [e4].[Id], [e3].[Id] AS [Id0], [e3].[EventsDate]
FROM [Planning] AS [e3]
INNER JOIN (
SELECT [e5].[Id], [e5].[StudentId], [e5].[PlanningId], [e6].[Id] AS [Id0]
FROM [Allocation] AS [e5]
INNER JOIN [Student] AS [e6] ON [e5].[StudentId] = [e6].[Id]
WHERE [e6].[Id] IN (
SELECT [s0].[value]
FROM OPENJSON(@__studentIds_0) WITH ([value] int ''$'') AS [s0]
)
) AS [t1] ON [e3].[Id] = [t1].[PlanningId]
INNER JOIN [Student] AS [e4] ON [t1].[StudentId] = [e4].[Id]
WHERE [e3].[EventsDate] = ''2024-04-04''
) AS [t0]
LEFT JOIN [Planning] AS [e7] ON [t0].[Id] = [e7].[Id]',N'@__studentIds_0 nvarchar(4000)',@__studentIds_0=N'[123]'
```
in this query we can observe that last `LEFT JOIN` is made to `[t0].[Id]` which corresponds to `Student.Id`, but it should be made to `Planning.Id`

### Additional notes:
* Marking owned type as required (`.Navigation(x => x.TimeRange).IsRequired()`) causes `SqlNullException`
* The same query worked in EF Core 6

### Workaround:
It is possible to replace the order of entities in returned anonymous type `(p, a) => new { a.Student, studentPlanning = p }` -> `(p, a) => new { studentPlanning = p, a.Student }` and the join is made to the correct (first) entity, but if both classes had owned types only one of them would work.

Full code:

```csharp
async Task Main()
{

var studentIds = new List { 123};
var c = new Context();

var planningsOnDate = c.Plannings.Where(p => p.EventsDate == new DateTime(2024, 04, 04));

var query = planningsOnDate.SelectMany(p =>
p.AutoAllocations.Where(a => studentIds.Contains(a.Student.Id)),
(p, a) => new { a.Student, studentPlanning = p }
//(p, a) => new { studentPlanning = p, a.Student }
)
.Union(
planningsOnDate.SelectMany(p =>
p.Allocations.Where(a => studentIds.Contains(a.Student.Id)),
(p, a) => new { a.Student, studentPlanning = p })
//(p, a) => new { studentPlanning = p, a.Student })
);

var results = await query.ToListAsync();

Console.WriteLine(results.First().studentPlanning.TimeRange); // null
}

public class Context : DbContext
{
public DbSet Plannings { get; set; }

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

modelBuilder.Entity(config =>
{
config.ToTable("Planning");

config.HasMany(c => c.Allocations).WithOne().HasForeignKey("PlanningId").OnDelete(DeleteBehavior.Cascade).IsRequired();
config.HasMany(c => c.AutoAllocations).WithOne().HasForeignKey("PlanningId").OnDelete(DeleteBehavior.Cascade).IsRequired();

config.Property(c => c.EventsDate).HasColumnType("date");

config.OwnsOne(x => x.TimeRange, x =>
{
x.Property(c => c.StartTime).HasColumnName("StartTime").IsRequired();
x.Property(c => c.EndTime).HasColumnName("EndTime").IsRequired();
});//.Navigation(x => x.TimeRange).IsRequired();
});

modelBuilder.Entity(config =>
{
config.ToTable("AutoAllocation");

config.HasOne(c => c.Student).WithMany().HasForeignKey("StudentId").IsRequired();
});

modelBuilder.Entity(config =>
{
config.ToTable("Allocation");

config.HasOne(c => c.Student).WithMany().HasForeignKey("StudentId").IsRequired();
});

modelBuilder.Entity(config =>
{
config.ToTable("Student");
});
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer("...")
.EnableSensitiveDataLogging(true);
}
}

public class Planning
{
public long Id { get; set; }

public DateTime EventsDate { get; set; }

public virtual List AutoAllocations { get; set; }

public virtual List Allocations { get; set; }

public virtual TimeRange TimeRange { get; set; }

}

public class AutoAllocation
{
public long Id { get; set; }

public virtual Student Student { get; set; }
}

public class Allocation
{
public long Id { get; set; }

public virtual Student Student { get; set; }
}

public class Student
{
public int Id { get; set; }
}

public class TimeRange
{
public string StartTime { get; set; }
public string EndTime { get; set; }
}

```

### Include provider and version information

EF Core version:
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8
EF Core Version: 8.0.4

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.