-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Closed as not planned
Closed as not planned
Copy link
Labels
Description
When using the Any()
function with a predicate that checks for a property being null
on an optional to-one navigation, a LEFT JOIN
is produced instead of an INNER JOIN
. The effect is that the returned result set contains too many matches. See the repro steps below.
This occurs with SQLite and PostgreSQL. I haven't tried other providers.
Include your code
#nullable enable
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
var services = new ServiceCollection();
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlite("Data Source=sample.db");
options.LogTo(Console.WriteLine, LogLevel.Information);
});
var serviceProvider = services.BuildServiceProvider();
await using (var scope = serviceProvider.CreateAsyncScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
dbContext.People.AddRange(
new Person
{
// No match expected: No assignee exists.
OwnedTodoItems =
{
new TodoItem()
}
},
new Person
{
// Match expected: Assignee.Age is null.
OwnedTodoItems =
{
new TodoItem
{
Assignee = new Person()
}
}
},
new Person
{
// No match expected: Assignee.Age is non-null.
OwnedTodoItems =
{
new TodoItem
{
Assignee = new Person
{
Age = 21
}
}
}
});
await dbContext.SaveChangesAsync();
}
await using (var scope = serviceProvider.CreateAsyncScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var query = dbContext.People
.Where(person => person.OwnedTodoItems.Any(todoItem => todoItem.Assignee!.Age == null));
/*
SELECT "p"."Id", "p"."Age"
FROM "People" AS "p"
WHERE EXISTS (
SELECT 1
FROM "TodoItems" AS "t"
LEFT JOIN "People" AS "p0" ON "t"."AssigneeId" = "p0"."Id"
WHERE "p"."Id" = "t"."OwnerId" AND ("p0"."Age" IS NULL))
*/
var people = await query.ToArrayAsync();
Console.WriteLine($"Found matches: {string.Join(", ", people.Select(person => person.Id))}");
}
public class TodoItem
{
public long Id { get; set; }
public Person Owner { get; set; } = null!;
public Person? Assignee { get; set; }
}
public class Person
{
public long Id { get; set; }
public int? Age { get; set; }
public HashSet<TodoItem> OwnedTodoItems { get; set; } = new();
public HashSet<TodoItem> AssignedTodoItems { get; set; } = new();
}
public class AppDbContext : DbContext
{
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
public DbSet<Person> People => Set<Person>();
public AppDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.HasMany(person => person.AssignedTodoItems)
.WithOne(todoItem => todoItem.Assignee);
modelBuilder.Entity<Person>()
.HasMany(person => person.OwnedTodoItems)
.WithOne(todoItem => todoItem.Owner);
}
}
The code above returns two matches instead of one:
Found matches: 1, 2
Changing the LEFT JOIN
to an INNER JOIN
produces a single match (2), which is expected.
Include provider and version information
EF Core version: v7.0.12
Database provider: Microsoft.EntityFrameworkCore.Sqlite and Npgsql.EntityFrameworkCore.PostgreSQL
Target framework: .NET 7
Operating system: Windows
IDE: Visual Studio 2022 17.7.5