You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SELECT*FROM People people
LEFT JOIN Employers employers ONemployers.EmployerId=people.EmployerIdLEFT JOIN People coworkers ONcoworkers.EmployerId=employers.EmployerIdWHEREpeople.Name= N'Name'
However, when you apply a filter, like
context.People.Where(person =>person.Name=="Name").Select(subject =>new{Name=subject.Name,Coworkers=subject.Employer!.Employees!.Where(employee =>employee!=subject)// changed, compared to above.Select(coworker =>new{coworker.Name}).ToList(),}).ToList();
this tranlates to a different join criteria:
SELECT*FROM People people
LEFT JOIN Employers employers ONemployers.EmployerId=people.EmployerIdLEFT JOIN People coworkers ON (coworkers.EmployerId=employers.EmployerIdOR (coworkers.EmployerId IS NULLANDemployers.EmployerId IS NULL)) ANDcoworkers.PersonId!=people.PersonIdWHEREpeople.Name= N'Name'
Expectation:
People without employer have an empty list of coworkers even with additional filter criteria.
Observation:
People without employer have all other people without employer listed as their coworkers.
This is not subject to the SQL Server provider. I was able to reproduce this with the most recent PostgreSQL provider, as well.
Your code
usingMicrosoft.EntityFrameworkCore;usingMicrosoft.EntityFrameworkCore.Migrations;publicclassEmployer{publicintEmployerId{get;set;}publicrequiredstringName{get;set;}publicIList<Person>?Employees{get;set;}}publicclassPerson{publicintPersonId{get;set;}publicrequiredstringName{get;set;}publicint?EmployerId{get;set;}publicEmployer?Employer{get;set;}}publicsealedclassTestContext:DbContext{publicDbSet<Employer>Employers{get;set;}publicDbSet<Person>People{get;set;}protectedoverridevoidOnConfiguring(DbContextOptionsBuilderoptionsBuilder){optionsBuilder.UseSqlServer("Data Source=localhost; Initial Catalog=Test; Integrated Security=True");}protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){varemployerBuilder=modelBuilder.Entity<Employer>();varpeopleBuilder=modelBuilder.Entity<Person>();peopleBuilder.HasKey(x =>x.PersonId);employerBuilder.HasKey(x =>x.EmployerId);employerBuilder.HasMany(x =>x.Employees).WithOne(x =>x.Employer).HasForeignKey(x =>x.EmployerId).HasPrincipalKey(x =>x.EmployerId);employerBuilder.HasData(newEmployer{EmployerId=1,Name="Microsoft"},newEmployer{EmployerId=2,Name="Apple"},newEmployer{EmployerId=3,Name="Google"});peopleBuilder.HasData(newPerson{PersonId=1,Name="Satya",EmployerId=1},newPerson{PersonId=2,Name="Brad",EmployerId=1},newPerson{PersonId=3,Name="Amy",EmployerId=1},newPerson{PersonId=4,Name="Tim",EmployerId=2},newPerson{PersonId=5,Name="Katherine",EmployerId=2},newPerson{PersonId=6,Name="Eddy",EmployerId=2},newPerson{PersonId=7,Name="Sundar",EmployerId=3},newPerson{PersonId=8,Name="Ruth",EmployerId=3},newPerson{PersonId=9,Name="Anat",EmployerId=3},newPerson{PersonId=10,Name="Donald",Employer=null},newPerson{PersonId=11,Name="Elon",Employer=null});}}[TestClass]publicsealedclassReproduction{[TestMethod]publicvoidEmployees_have_coworkers(){usingvarcontext=newTestContext();context.Database.EnsureCreated();varsatya=context.People.Where(person =>person.Name=="Satya").Select(satya =>new{Name=satya.Name,Coworkers=satya.Employer!.Employees!.Where(employee =>employee!=satya).Select(coworker =>new{coworker.Name}).ToList(),}).Single();Assert.IsTrue(satya.Coworkers.Count==2);Assert.IsTrue(satya.Coworkers.Any(coworker =>coworker.Name=="Brad"));Assert.IsTrue(satya.Coworkers.Any(coworker =>coworker.Name=="Amy"));}[TestMethod]publicvoidUnemployed_people_do_not_have_coworkers(){usingvarcontext=newTestContext();context.Database.EnsureCreated();vardonald=context.People.Where(person =>person.Name=="Donald").Select(donald =>new{Name=donald.Name,Coworkers=donald.Employer!.Employees!.Where(employee =>employee!=donald).Select(coworker =>new{coworker.Name}).ToList(),}).Single();Assert.IsTrue(donald.Coworkers.Count==0);// This fails. Donald and Elon are coworkers.}}publicclassTestMigration:Migration{protectedoverridevoidUp(MigrationBuildermigrationBuilder){migrationBuilder.CreateTable(name:"Employers",columns: table =>new{EmployerId=table.Column<int>("int",false).Annotation("SqlServer:Identity","1, 1"),Name=table.Column<string>("nvarchar(max)",false)},constraints: table =>{table.PrimaryKey("PK_Employers", x =>x.EmployerId);});migrationBuilder.CreateTable(name:"People",columns: table =>new{PersonId=table.Column<int>("int",false).Annotation("SqlServer:Identity","1, 1"),Name=table.Column<string>("nvarchar(max)",false),EmployerId=table.Column<int>("int",true)},constraints: table =>{table.PrimaryKey("PK_People", x =>x.PersonId);table.ForeignKey("FK_People_Employers_EmployerId", x =>x.EmployerId,"Employers","EmployerId");});migrationBuilder.InsertData(table:"Employers",columns:["EmployerId","Name"],values:newobject[,]{{1,"Microsoft"},{2,"Apple"},{3,"Google"}});migrationBuilder.InsertData(table:"People",columns:["PersonId","EmployerId","Name"],values:newobject?[,]{{10,null,"Donald"},{11,null,"Elon"},{1,1,"Satya"},{2,1,"Brad"},{3,1,"Amy"},{4,2,"Tim"},{5,2,"Katherine"},{6,2,"Eddy"},{7,3,"Sundar"},{8,3,"Ruth"},{9,3,"Anat"}});}}
Stack traces
Verbose output
EF Core version
9.0.2
Database provider
Microsoft.EntityFrameworkCore.SqlServer 9.0.2
Target framework
.net 9.0
Operating system
Windows 10
The text was updated successfully, but these errors were encountered:
I'm able to reproduce this. Initially we generate the correct condition
WHERE ((e.EmployerId!=NULL) && (e.EmployerId==p2.EmployerId)) && (p2.PersonId!=p.PersonId)
but in the apply projection step, when adding a join we go through RemoveRedundantNullChecks, which removes the null check.
When we get to nullability processor we have:
SELECT s.Name, s.PersonId, s.EmployerId, p0.Name, p0.PersonId
FROM
(
SELECT TOP(2) p.Name, p.PersonId, e.EmployerId
FROM People AS p
LEFT JOIN Employers AS e ON p.EmployerId == e.EmployerId
WHERE p.Name == N'Donald'
) AS s
LEFT JOIN People AS p0 ON (s.EmployerId == p0.EmployerId) && (s.PersonId != p0.PersonId)
ORDER BY s.PersonId ASC, s.EmployerId ASC
in null semantics when we process nullability for join predicate, if the binary expression is not Equal, we expand to c# null semantics. This is to mimic joins on anonymous types, stuff like:
selectxfromtable1join y from table2 on { Foo = x.Id, Bar =null} equals { Foo =1, Bar =null}select ...
When there is no extra condition (.Where(employee => employee != subject) from the example), we end up with a simple join predicate, which should follow relational null semantics and is therefore not expanded.
we could improve the way we process composite join predicates in nullability processor - e.g. we could slash matching IS NULL on both sides (but that leaves comparison to a null-value column, which we can't simplify) We could also make sure that predicate has a structure corresponding to anonymous object comparison ( a = b AND c = d AND ...), but that also doesn't cover all the cases.
alternatively we could keep the initial "redundant" null check and only slash it after null semantics, but need to investigate more whether it is safe
Bug description
The attempt to use a nullable navigation property to iterate over related records seems to work only when unfiltered.
Based on the provided code, EF seems to translate the code
to something which narrows down to
However, when you apply a filter, like
this tranlates to a different join criteria:
Expectation:
Observation:
This is not subject to the SQL Server provider. I was able to reproduce this with the most recent PostgreSQL provider, as well.
Your code
Stack traces
Verbose output
EF Core version
9.0.2
Database provider
Microsoft.EntityFrameworkCore.SqlServer 9.0.2
Target framework
.net 9.0
Operating system
Windows 10
The text was updated successfully, but these errors were encountered: