投影中使用ToList()是否单独执行?EF Core行为解析
我维护的遗留代码库有大量仓储方法存在线程阻塞问题。我知道在IQueryable上调用ToList()、FirstOrDefault()会同步从数据库取数据,阻塞线程直到收到响应,但不确定投影中使用ToList()或FirstOrDefault()是否也会有同样的阻塞表现。
示例代码如下(外层用了异步方法,但我怀疑投影内的ToList()并非异步执行):
var data = await (from item in Items where item.IsDeleted == false orderby item.Name ascending select new { Id = item.Id, Name = item.Name, MakeId = item.MakeId, Properties = (from ip in ItemProperties join prop in Properties on ip.PropertyId equals prop.Id where ip.ItemId == item.Id && ip.IsDeleted == false && prop.IsDeleted == false select new { Id = prop.Id, Name = prop.Name, }).ToList(), }).ToListAsync();
我用LinqPad诊断查询,一开始没看到投影内ToList()对应的SQL逻辑,不清楚EF Core是怎么解析它的。后来拿到了LinqPad生成的SQL,看起来投影里的查询并没有单独执行:
SELECT i."Id", i."Name", i."MakeId", t."Id", t."Name", t."Id0" FROM "Items" AS i LEFT JOIN ( SELECT p."Id", p."Name", i0."Id" AS "Id0", i0."ItemId" FROM "ItemProperties" AS i0 INNER JOIN "Properties" AS p ON i0."PropertyId" = p."Id" WHERE (i0."IsDeleted" = FALSE) AND (p."IsDeleted" = FALSE) ) AS t ON i."Id" = t."ItemId" WHERE i."IsDeleted" = FALSE ORDER BY i."Name", i."Id", t."Id0", t."Id"
核心结论与解析
投影内的
ToList()不会触发同步查询,也不会阻塞线程
EF Core会将投影中的子查询与主查询合并为一条完整的SQL语句(即你看到的带LEFT JOIN的SQL),所有数据一次性从数据库拉取。这里的ToList()只是告诉EF Core:将关联的Properties数据打包为集合,映射到匿名类的Properties属性,它并不是实际的查询执行调用,不会单独触发同步数据库操作。异步执行的保障
整个查询最终通过ToListAsync()执行,这是异步方法,会释放当前线程直到数据库返回结果。投影内的ToList()仅参与EF Core的查询翻译逻辑,不会产生同步阻塞。需警惕的真正阻塞场景
只有当你在投影中先调用AsEnumerable()/ToList()将数据拉到内存,再执行后续LINQ操作(比如内存过滤、转换)时,EF Core才会同步拉取子查询数据,此时才会出现线程阻塞。你的代码中不存在这种情况,子查询完全是EF Core可翻译的SQL逻辑。从生成SQL验证
从你提供的SQL可以看出,EF Core将Items与关联表做了LEFT JOIN,一次性查询所有所需数据,之后在内存中自动将关联的Properties数据分组映射到每个Item的Properties集合中。整个过程仅执行一次异步查询,无额外同步调用。
内容的提问来源于stack exchange,提问作者Elham Kohestani

