升级到EFCore8后,Contains()在SQLServer中因翻译为CTE时缺少前置分号而报错。原因是EFCore8改用CTE/OPENJSON传递参数集合。可通过FromSqlRaw参数化查询、FindAsync或内存过滤等方案规避。
最近不少团队在升级到 .NET 8 和 EF Core 8 后,遇到了一个常见的 EF Core 8 Contains 查询问题:原来跑得好好的 Where(x => ids.Contains(x.id)) 突然抛出异常,日志里赫然写着:“关键字 'WITH' 附近有语法错误。如果此语句是公用表表达式,那么前一个语句必须以分号结尾。”——这篇文章把 EF Core 8 Contains 错误的根本原因、修复方式以及以后怎么写才不会踩坑,一次性说清楚。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
假设你有这样一段再普通不过的代码:
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。
这不是 Bug,这是 EF Core 8 的一个 有意为之的 Breaking Change(官方文档明确定义为 High Impact)。
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,没有任何问题——直到你开始关注查询计划缓存。
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。
微软在 EF Core 8 Breaking Changes 中明确记录了这条(Tracking Issue #13617):
Containsin LINQ queries may stop working on older SQL Server versions
Impact: High
不是所有 Contains 都会炸,但它会在不经意间冒出来。触发条件包括但不限于:
| 场景 | 风险 |
|---|---|
ids.Contains(x.id) 且 ids 是 List | 高 |
ids.Contains(x.id) 且 ids 是 List | 高(不少项目遇到的) |
stringList.Contains(x.name) | 中(可能走 OPENJSON) |
同一查询中有多个 Contains | 高 |
.Contains() 嵌套在复杂 Where 表达式中 | 中 |
| 查询中同时有其他关联(Join / Include) | 高 |
最重要的信号:一旦看到错误信息里出现 WITH 和 分号,99% 就是这个问题。
直接绕过 EF 翻译,用 FromSqlRaw + SqlParameter,性能最优,零坑:
var paramNames = ids.Select((_, i) => $"@p{i}").ToArray();
var parameters = ids.Select((id, i) =>
new Microsoft.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();
WHERE id IN (@p0, @p1, ...)Microsoft.Data.SqlClient 随 EF Core SQL Server 包引入,无需另装适用:批量删除、批量更新、批量查询等「已知 ID 列表查实体」场景。
如果 ID 列表很短(比如页面批量操作选 10-20 条),直接用主键查:
var users = new List();
foreach (var id in ids)
{
var user = await _db.sys_admin.FindAsync(id);
if (user != null) users.Add(user);
}
FindAsync 走主键索引直查,不生成 CTE适用:后台管理的批量操作(用户勾选几条记录删除/启用等),ID 数量通常不超过几十个。
var all = await _db.sys_admin.ToListAsync();
var users = all.Where(x => ids.Contains(x.id)).ToList();
Contains 在内存中走 LINQ to Objects,没有 SQL 问题适用:字典表、配置表等行数很少(<1000)的表。
有时候 List 换成 int[] 后,EF 生成的 SQL 就不同了:
var idArray = ids.ToArray();
var users = await _db.sys_admin
.Where(x => idArray.Contains(x.id))
.ToListAsync();
如果你已经升级到 EF Core 9,可以用新增的配置项恢复旧行为:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(connectionString)
.TranslateParameterizedCollectionsToConstants();
}
或者:
builder.Services.AddDbContext(options =>
options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.TranslateParameterizedCollectionsToConstants()));
你的 SQL Server 版本 >= 2016 且兼容级别 >= 130?
└─ 是 → 方案一(FromSqlRaw)或方案二(FindAsync),保持 EF8 的新行为
└─ 否 → 考虑升级 SQL Server 或兼容级别
你的项目还在 .NET 8 / EF Core 8?
└─ 批量操作(已知 IDs)→ 方案一 或 方案二
└─ 动态查询(用户输入过滤)→ 直接用 EF8 的 OPENJSON 方式不会触发 CTE 问题
└─ 小表兜底 → 方案三
你的项目已升级到 EF Core 9?
└─ 考虑方案五,但要理解它的代价(查询计划缓存退化)
在 MiePcb 管理后台项目中先后踩了两次这个坑:
GetPageList 的部门查询// 错误写法
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();
// 后续在内存中关联
BatchDelete 的批量删除// 错误写法
var users = await _db.sys_admin.Where(x => ids.Contains(x.id)).ToListAsync();
报错:同上。
修复:方案一(FromSqlRaw + 参数化),一次查询干净利落。
Contains 都要在心里打个问号 — 写的时候就要想好万一报错走哪个方案List 比 List 更容易触发 — 可空类型让 EF 生成的 SQL 更复杂,更倾向 CTEWITH 就看 Contains — 排查方向比错误信息本身更重要一句话总结:EF Core 8 把
Contains翻译从"内联常量"改成了"CTE / OPENJSON",CTE 要求前面有分号但 EF 没补,SQL Server 就炸了。修起来简单——用FromSqlRaw参数化查询、FindAsync、或者内存过滤。核心原则:不再盲目信任 EF 的Contains翻译,批量操作优先 Raw SQL。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述