var ids new List { 1, 2, 3, 5, 8 };var users await _db.sys_admin.Where(x ids.Contains(x.id)).ToListAsync();在 EF Core 6/7 上完全正常。升级到 EF Core 8 后同样的代码报Microsoft.Data.SqlClient.SqlException (0x80131904):关键字 ‘WITH’ 附近有语法错误。如果此语句是公用表表达式、xmlnamespaces 子句或者更改跟踪上下文子句那么前一个语句必须以分号结尾。关键词WITH、分号、公用表表达式CTE。SQL Server 错误号为 156。根因EF Core 8 对 Contains 的翻译方式变了这不是 Bug这是 EF Core 8 的一个 有意为之的 Breaking Change官方文档明确定义为 High Impact。2.1 旧行为EF Core 6/7EF 把参数化列表的值内联为 SQL 常量– EF Core 7 生成的 SQLSELECT [s].[id], [s].[username], …FROM [sys_admin] AS [s]WHERE [s].[id] IN (1, 2, 3, 5, 8)简单直接没有 CTE没有任何问题——直到你开始关注查询计划缓存。2.2 新行为EF Core 8EF Core 8 不再内联常量而是通过 OPENJSON 或 CTE公用表表达式 来传递参数化集合。简化后的生成逻辑是简单值列表string/int 常量→ OPENJSON 方式复杂查询 / 多次 Contains → CTEWITH … AS方式对于 ids.Contains(x.id) 这类场景EF 可能生成类似这样的 SQL– EF Core 8 可能生成的 SQL简化版;WITH [t] AS (SELECT [v].[value] FROM OPENJSON(__ids_0) …)SELECT [s].[id], …FROM [sys_admin] AS [s]WHERE [s].[id] IN (SELECT [t].[value] FROM [t])问题来了WITH 前面必须有一个完整语句的分号 ;。如果当前 SQL 批处理中 EF 没有在前面补上分号SQL Server 就会报错 156。2.3 官方文档怎么说微软在 EF Core 8 Breaking Changes 中明确记录了这条Tracking Issue #13617Contains in LINQ queries may stop working on older SQL Server versionsImpact: High链接learn.microsoft.com - Breaking changes in EF Core 8.0什么时候会触发不是所有 Contains 都会炸但它会在你不经意间冒出来。触发条件包括但不限于场景 风险ids.Contains(x.id) 且 ids 是 List 高ids.Contains(x.id) 且 ids 是 Listint? 高我们项目遇到的stringList.Contains(x.name) 中可能走 OPENJSON同一查询中有多个 Contains 高.Contains() 嵌套在复杂 Where 表达式中 中查询中同时有其他关联Join / Include 高最重要的信号一旦看到错误信息里出现 WITH 和 分号99% 就是这个问题。解决方案5 种从优到差方案一参数化 Raw SQL推荐 ⭐直接绕过 EF 翻译用 FromSqlRaw SqlParameter性能最优零坑var paramNames ids.Select((_, i) pi).ToArray();varparametersids.Select((id,i)newMicrosoft.Data.SqlClient.SqlParameter(p{i}).ToArray(); var parameters ids.Select((id, i) new Microsoft.Data.SqlClient.SqlParameter(pi).ToArray();varparametersids.Select((id,i)newMicrosoft.Data.SqlClient.SqlParameter(“p{i}”, id)).ToArray();var sql $“SELECT * FROM sys_admin WHERE id IN ({string.Join(”,“, paramNames)})”;var users await _db.sys_admin.FromSqlRaw(sql, parameters).ToListAsync();✅ 生成的 SQL 就是简单的 WHERE id IN (p0, p1, …)✅ Microsoft.Data.SqlClient 随 EF Core SQL Server 包引入无需另装✅ 完全防注入⚠️ 需要知道表名但你的 DbContext 本来就定义了适用批量删除、批量更新、批量查询等「已知 ID 列表查实体」场景。方案二FindAsync 逐个查询小数据量 ⭐如果 ID 列表很短比如页面批量操作选 10-20 条直接用主键查var users new Listsys_admin();foreach (var id in ids){var user await _db.sys_admin.FindAsync(id);if (user ! null) users.Add(user);}✅ FindAsync 走主键索引直查不生成 CTE✅ 简单可靠❌ N1 查询ID 数量多时性能差适用后台管理的批量操作用户勾选几条记录删除/启用等ID 数量通常不超过几十个。方案三全量拉到内存过滤小表 ⭐var all await _db.sys_admin.ToListAsync();var users all.Where(x ids.Contains(x.id)).ToList();✅ 零 SQL 风险✅ 一句话搞定❌ 全表拉到内存表大了就是灾难❌ Contains 在内存中走 LINQ to Objects没有 SQL 问题适用字典表、配置表等行数很少1000的表。方案四ToArray()碰运气有时候 List 换成 int[] 后EF 生成的 SQL 就不同了var idArray ids.ToArray();var users await _db.sys_admin.Where(x idArray.Contains(x.id)).ToListAsync();⚠️ 不保证有效取决于具体的 EF Core 8.x 小版本和查询复杂度⚠️ 同一套代码在不同环境可能表现不一致❌ 不推荐作为可靠方案方案五EF 9 专属TranslateParameterizedCollectionsToConstants如果你已经升级到 EF Core 9可以用新增的配置项恢复旧行为protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlServer(connectionString).TranslateParameterizedCollectionsToConstants();}或者builder.Services.AddDbContext(options options.UseSqlServer(connectionString,sqlOptions sqlOptions.TranslateParameterizedCollectionsToConstants()));✅ 全局生效一行配置解决✅ 恢复 EF Core 7 的 Contains 翻译方式❌ 回到旧行为查询计划缓存问题依旧存在微软当初改它的原因❌ 需要 EF Core 95. 推荐策略你的 SQL Server 版本 2016 且兼容级别 130└─ 是 → 方案一FromSqlRaw或方案二FindAsync保持 EF8 的新行为└─ 否 → 考虑升级 SQL Server 或兼容级别你的项目还在 .NET 8 / EF Core 8└─ 批量操作已知 IDs→ 方案一 或 方案二└─ 动态查询用户输入过滤→ 直接用 EF8 的 OPENJSON 方式不会触发 CTE 问题└─ 小表兜底 → 方案三你的项目已升级到 EF Core 9└─ 考虑方案五但要理解它的代价查询计划缓存退化6. 实战案例MiePcb 项目经验我们在 MiePcb 管理后台项目中先后踩了两次这个坑踩坑 1GetPageList 的部门查询// 错误写法query query.Where(x deptIds.Contains(x.dept_id));报错WITH 附近语法错误。修复内存全量拉取部门表再 Join 过滤。var depts await _db.sys_dept.Select(d new { d.id, d.name }).ToListAsync();// 后续在内存中关联踩坑 2BatchDelete 的批量删除// 错误写法var users await _db.sys_admin.Where(x ids.Contains(x.id)).ToListAsync();报错同上。修复方案一FromSqlRaw 参数化一次查询干净利落。经验总结EF Core 8 的项目所有 Contains 都要在心里打个问号 — 写的时候就要想好万一报错走哪个方案Listint? 比 List 更容易触发 — 可空类型让 EF 生成的 SQL 更复杂更倾向 CTESQL Server 错误 156 看到 WITH 就看 Contains — 排查方向比错误信息本身更重要记录到 MEMORY.md — 团队其他人可能不知道这个坑要有文档沉