Union/Concat returning more than one entity with owned types generates incorrect joins
- 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
Assessment
This issue has not been assessed yet.