使用GroupJoin执行LEFT JOIN时抛出错误(疑似Bug?)
问题背景
我需要用EF Core实现简单的LEFT JOIN,效果和SQL一致:键存在时返回对应行,不存在时返回NULL,且必须用Fluent调用(项目其余部分均采用该方式)。
以下查询语法写法可以正常运行:
var entites = await ( from b in Context.Buildings join r in Context.Regions on b.BuildingId equals r.BuildingId into regions from r in regions.DefaultIfEmpty() select new { b, r } ).ToListAsync();
但下面的Fluent写法编译正常,运行时却抛出错误:
var entities = await Context.Buildings .GroupJoin( Context.Regions, building => building.BuildingId, region => region.BuildingId, (building, region) => new { building, region } ) .SelectMany(z => z.region.DefaultIfEmpty(), (building, region) => new { building, region }) .ToListAsync();
错误信息:
System.InvalidOperationException: variable 'region' of type 'System.Collections.Generic.IEnumerable`1[Sql.Models.Region]' referenced from scope '', but it is not defined
这里的region是GroupJoin参数列表中(building,region)的第二个参数,我尝试了多种方法都无法让程序识别该参数。当前使用EF Core 6.0.4,按道理多年前的相关Bug应该已经修复了,这是怎么回事?
错误原因分析
问题出在SelectMany的第二个委托参数命名上:你在这个委托里用了building作为第一个参数名,但这个参数实际上对应GroupJoin输出的匿名类型对象(也就是SelectMany的源元素,即代码里的z),而不是原Building实体。同时,你在GroupJoin里定义的匿名类型属性也叫building,这就导致了命名冲突,EF Core的表达式解析器混淆了不同作用域的变量,从而抛出未定义的错误。
正确的Fluent写法
调整SelectMany的参数命名,明确区分不同作用域的变量,同时确保投影的对象正确:
var entities = await Context.Buildings .GroupJoin( Context.Regions, building => building.BuildingId, region => region.BuildingId, (building, regions) => new { Building = building, Regions = regions } ) .SelectMany( joined => joined.Regions.DefaultIfEmpty(), (joined, region) => new { joined.Building, Region = region } ) .ToListAsync();
写法说明
- GroupJoin的结果匿名类型用
Building和Regions作为属性名,避免后续命名冲突 - SelectMany的第一个参数明确用
joined指代GroupJoin的输出对象,遍历其Regions集合并调用DefaultIfEmpty()实现LEFT JOIN逻辑 - SelectMany的第二个参数中,
joined是GroupJoin的输出对象,region是当前遍历到的Region(可能为null),最后投影出包含Building和Region的匿名类型
这样写法就和查询语法的逻辑完全一致,能够正确生成LEFT JOIN的SQL语句,运行时也不会再抛出变量未定义的错误。
内容的提问来源于stack exchange,提问作者David Dombrowsky

