-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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.
var studentIds = new List<int> { 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); // nullIn the above query Planning entity has an owned type TimeRange with StartTime and EndTime properties
generated query:
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()) causesSqlNullException - 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:
async Task Main()
{
var studentIds = new List<int> { 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<Planning> Plannings { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Planning>(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<AutoAllocation>(config =>
{
config.ToTable("AutoAllocation");
config.HasOne(c => c.Student).WithMany().HasForeignKey("StudentId").IsRequired();
});
modelBuilder.Entity<Allocation>(config =>
{
config.ToTable("Allocation");
config.HasOne(c => c.Student).WithMany().HasForeignKey("StudentId").IsRequired();
});
modelBuilder.Entity<Student>(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<AutoAllocation> AutoAllocations { get; set; }
public virtual List<Allocation> 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