GroupJoin in EF Core 9/10 returns single row for SQL Server temporal tables
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
Use case: Get one entity by Id (PK), and all its history from the temporal table.
When I'm trying to collect the historical rows, EF Core returns only a single history row when using `.TemporalAll()` and `GroupJoin` together, however the generated SQL query looks fine. I've attached a runnable `Program.cs` that reproduces the problem.
Note, that the same problem can be observed when manual left join is used (see in code example), the generated SQL query is the same.
The problem happens with EF Core versions 9.0.11 and 10.0.0.
### Your code
```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
var options = new DbContextOptionsBuilder()
.UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=Ef10TemporalSample;Integrated Security=true;")
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine, LogLevel.Information)
.Options;
int blogId;
await using (var context = new MyContext(options))
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
Blog[] blogs = [
new Blog { Url = "https://1.blog.com" },
new Blog { Url = "https://2.blog.com" }
];
context.Blogs.AddRange(blogs);
await context.SaveChangesAsync();
var updateBlog = blogs[0];
updateBlog.Url = "https://foo.blog.com";
await context.SaveChangesAsync();
updateBlog.Url = "https://bar.blog.com";
await context.SaveChangesAsync();
// Note the updated blog id.
blogId = updateBlog.Id;
}
var expectedHistoryCount = 3; // 1 create + 2 update
await using (var context = new MyContext(options))
{
var actualCount = await context.Blogs.TemporalAll()
.Where(x => x.Id == blogId)
.CountAsync();
Console.WriteLine($"=== Temporal table direct query: Expected {expectedHistoryCount}, actual {actualCount}");
}
await using (var context = new MyContext(options))
{
var blogWithHistory = await context.Blogs
.Where(x => x.Id == blogId)
.GroupJoin(
context.Blogs.TemporalAll(),
blog => blog.Id,
history => history.Id,
(blog, histories) => new
{
blog.Id,
History = histories
.Select(x => new
{
From = EF.Property(x, "ValidFrom"),
To = EF.Property(x, "ValidTo"),
x.Url
})
.ToArray()
})
.FirstAsync();
Console.WriteLine($"=== GroupJoin: Expected {expectedHistoryCount}, actual {blogWithHistory.History.Length}");
}
await using (var context = new MyContext(options))
{
var blogWithHistory = await context.Blogs
.Where(x => x.Id == blogId)
.Select(blog => new
{
blog.Id,
History = context.Blogs.TemporalAll()
.Where(x => x.Id == blog.Id)
.Select(x => new
{
From = EF.Property(x, "ValidFrom"),
To = EF.Property(x, "ValidTo"),
x.Url
})
.ToArray()
})
.FirstAsync();
Console.WriteLine($"=== LEFT JOIN: Expected {expectedHistoryCount}, actual {blogWithHistory.History.Length}");
}
class MyContext : DbContext
{
public MyContext(DbContextOptions options)
: base(options)
{
}
public DbSet Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(e =>
{
e.ToTable(
"Blogs",
table => table.IsTemporal(x =>
{
x.UseHistoryTable("BlogHistory");
x.HasPeriodStart("ValidFrom");
x.HasPeriodEnd("ValidTo");
}));
e.HasKey(x => x.Id);
});
}
}
class Blog
{
public int Id { get; set; }
public required string Url { get; set; }
}
```
### Stack traces
```text
```
### Verbose output
```text
info: 2025. 12. 03. 11:17:12.556 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (6ms) [Parameters=[@blogId='1'], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*)
FROM [Blogs] FOR SYSTEM_TIME ALL AS [b]
WHERE [b].[Id] = @blogId
=== Temporal table direct query: Expected 3, actual 3
info: 2025. 12. 03. 11:17:12.680 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (4ms) [Parameters=[@blogId='1'], CommandType='Text', CommandTimeout='30']
SELECT [b1].[Id], [b0].[ValidFrom], [b0].[ValidTo], [b0].[Url], [b0].[Id]
FROM (
SELECT TOP(1) [b].[Id]
FROM [Blogs] AS [b]
WHERE [b].[Id] = @blogId
) AS [b1]
LEFT JOIN [Blogs] FOR SYSTEM_TIME ALL AS [b0] ON [b1].[Id] = [b0].[Id]
ORDER BY [b1].[Id]
=== GroupJoin: Expected 3, actual 1
info: 2025. 12. 03. 11:17:12.702 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[@blogId='1'], CommandType='Text', CommandTimeout='30']
SELECT [b1].[Id], [b0].[ValidFrom], [b0].[ValidTo], [b0].[Url], [b0].[Id]
FROM (
SELECT TOP(1) [b].[Id]
FROM [Blogs] AS [b]
WHERE [b].[Id] = @blogId
) AS [b1]
LEFT JOIN [Blogs] FOR SYSTEM_TIME ALL AS [b0] ON [b1].[Id] = [b0].[Id]
ORDER BY [b1].[Id]
=== LEFT JOIN: Expected 3, actual 1
```
### EF Core version
10.0.0
### Database provider
Microsoft.EntityFrameworkCore.SqlServer
### Target framework
.NET 10
### Operating system
Windows 11
### IDE
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.