dotnet / dotnet/efcore

IQueryable containing .DefaultIfEmpty() does INNER JOIN instead of LEFT JOIN, when used as a sub query

Open
#34,236 4 comments 0 reactions 0 assignees View on GitHub
area-query customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

[ConsoleQueryApp.zip](https://github.com/user-attachments/files/16249004/ConsoleQueryApp.zip)

### Introduction

This is probably related to issue https://github.com/dotnet/efcore/issues/27480

The wrong query seems to be generated which also returns the wrong results.

I'm using 3 tables in my simplified examples (Emp, Dep, EmpDep), to demonstrate the problem (i've attached the full example with the MS SQL Server definitions in ConsoleQueryApp.zip). The Emp and Dep tables have a single column "Id" and the EmpDep table is empty:

Emp (table)Dep (table)EmpDep (table)

| Id |
| -- |
| 1 |
| 2 |

| Id |
| -- |
| 3 |
| 4 |
| 5 |

| EmpId | DepId |
| ------ | ----- |


I have an IQueryable containing a LEFT JOIN using .DefaultIfEmpty():

MyDbContext db = new MyDbContext(); // the DbContext

// IQueryable
var subQ =
from e in db.Emps
from d in db.Deps
from ed in db.EmpDeps.Where(ed => ed.EmpId == e.Id).DefaultIfEmpty() // left join
select new
{ EmpId = e.Id
, DepId = d.Id
};

int myId = 1;

If i directly use ToList() on it, then the generated SQL uses a LEFT JOIN (as expected):
```C#
// edsL = [ {EmpId=1,DepId=3}, {EmpId=1,DepId=4}, {EmpId=1,DepId=5}
// , {EmpId=2,DepId=3}, {EmpId=2,DepId=4}, {EmpId=2,DepId=5}]
var edsL = subQ.ToList();
```

SELECT [e].[Id] AS [EmpId], [d].[Id] AS [DepId]

FROM [Emp] AS [e]
CROSS JOIN [Dep] AS [d]
LEFT JOIN [EmpDep] AS [e0] ON [e].[Id] = [e0].[EmpId]

### Short Syntax
But when i use it as a sub query, then the generated SQL uses an INNER JOIN in the sub query (NOT as expected):
```C#
// xs = [{EmpId=1,DepId=null}] (bad) (short syntax)
var xs =
(from e in db.Emps
from ed in subQ.Where(ed => ed.EmpId == e.Id).DefaultIfEmpty() // left join
where e.Id == myId
select new
{ EmpId = e.Id
, DepId = (ed == null ? (int?)null : ed.DepId)
})
.ToList();
```

exec sp_executesql N'SELECT [e].[Id] AS [EmpId], [t].[DepId]

FROM [Emp] AS [e]
LEFT JOIN (
SELECT [e0].[Id] AS [EmpId], [d].[Id] AS [DepId]
FROM [Emp] AS [e0]
CROSS JOIN [Dep] AS [d]
INNER JOIN [EmpDep] AS [e1] ON [e0].[Id] = [e1].[EmpId]
) AS [t] ON [e].[Id] = [t].[EmpId]
WHERE [e].[Id] = @__myId_0',N'@__myId_0 int',@__myId_0=1

i was expecting a SQL LEFT JOIN or maybe an OUTER APPLY (which is similar), in the sub query. the result also seems wrong.

### LINQ-to-Objects
When i first convert the subquery into a list and use LINQ-to-Objects instead of EF Core, then i get the expected result:
```C#
List es = db.Emps.ToList();

// ys = [{EmpId=1,DepId=3}, {EmpId=1,DepId=4}, {EmpId=1,DepId=5}] (good) (with LINQ-to-Objects)
var ys =
(from e in es
from ed in edsL.Where(ed => ed.EmpId == e.Id).DefaultIfEmpty() // left join
where e.Id == myId
select new
{ EmpId = e.Id
, DepId = (ed == null ? (int?)null : ed.DepId)
})
.ToList();
```

### Long Syntax
What seems to prevent the generation of SQL INNER JOIN, is by doing the left join using longer "join into" syntax, in the IQueryable:
```C#
// IQueryable
var subQ2 =
from e in db.Emps
from d in db.Deps
join ed in db.EmpDeps on e.Id equals ed.EmpId into eds
from ed in eds.DefaultIfEmpty() // left join
select new
{ EmpId = e.Id
, DepId = d.Id
};

// xs2 = [{EmpId=1,DepId=3}, {EmpId=1,DepId=4}, {EmpId=1,DepId=5}] (good) (with long syntax)
var xs2 =
(from e in db.Emps
from ed in subQ2.Where(ed => ed.EmpId == e.Id).DefaultIfEmpty() // left join
where e.Id == myId
select new
{ EmpId = e.Id
, DepId = (ed == null ? (int?)null : ed.DepId)
})
.ToList();
```

exec sp_executesql N'SELECT [e].[Id] AS [EmpId], [t].[DepId]

FROM [Emp] AS [e]
LEFT JOIN (
SELECT [e0].[Id] AS [EmpId], [d].[Id] AS [DepId]
FROM [Emp] AS [e0]
CROSS JOIN [Dep] AS [d]
LEFT JOIN [EmpDep] AS [e1] ON [e0].[Id] = [e1].[EmpId]
) AS [t] ON [e].[Id] = [t].[EmpId]
WHERE [e].[Id] = @__myId_0',N'@__myId_0 int',@__myId_0=1

### Observation
My expectation was that the result of an IQueryable is not influenced, by whether or not it is used as a sub query.
* is this a bug in EF?
* can someone explain what's going on here?

### Provider and version information

EF Core version: Microsoft.EntityFrameworkCore.Tools 8.0.7
Database provider: Microsoft.EntityFrameworkCore.SqlServer 8.0.7
Target framework: .NET 8.0
Operating system: Windows Server 2019 Datacenter (version 1809)
IDE: Visual Studio 2022 Pro 17.9.6

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.