近段时间,许多开发团队在将项目迁移至 .NET 8 与 EF Core 8 之后,遇到了一个令人困惑的查询异常:原本运行正常的 Where(x => ids.Contains(x.id)) 语句突然抛出错误,日志中清晰显示:“关键字 'WITH' 附近有语法错误。如果此语句是公用表表达式,那么前一个语句必须以分号结尾。”——本文将从根本原因、修复方式到未来的最佳实践,一次性为你梳理清楚。

1. 首先了解症状表现
假设你有一段非常常见的代码:
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。
2. 根因分析:EF Core 8 对 Contains 的翻译机制发生了变更
这不是所谓的 Bug,而是 EF Core 8 引入的一个 有意为之的重大变更(Breaking Change)(官方文档明确标注为 High Impact)。
2.1 旧版本行为(EF Core 6/7)
EF 会将参数化列表的值直接内联为 SQL 常量:
-- EF Core 7 生成的 SQL
SELECT [s].[id], [s].[username], ...
FROM [sys_admin] AS [s]
WHERE [s].[id] IN (1, 2, 3, 5, 8)
这种翻译方式简单直接,没有使用 CTE,也不会出现任何问题——除非你开始关注查询计划缓存。
2.2 新版本行为(EF Core 8)
EF Core 8 不再采用内联常量的方式,改为通过 OPENJSON 或 CTE(公用表表达式) 来传递参数化集合。简化后的生成逻辑大致如下:
简单值列表(如 string/int 常量)→ 采用 OPENJSON 方式
复杂查询或多次 Contains → 采用 CTE(WITH ... 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 #13617):
Containsin LINQ queries may stop working on older SQL Server versions
Impact: High
3. 哪些场景会触发此错误?
并非所有 Contains 都会引发问题,但它可能在意想不到的时刻出现。触发条件包括但不限于:
