EF Core 8 + SQL Server:Contains() 突然报 "关键字 WITH 附近有语法错误"?一篇避坑指南

大家好,我是码农刚子,最近遇到一个EF Core 查询的问题,跟大家分享下。 升级到 .NET 8 / EF Core 8 后,原来跑得好好的 Where(x => ids.Contains(x.id)) 突然炸了,日志里赫然写着:关键字 'WITH' 附近有语法错误。如果此语句是公用表表达式,那么前一个语句必须以分号结尾。 ------这篇文章帮你搞清楚为什么、怎么修、以后怎么写才不踩坑。

1. 先看症状

假设你有这样一段再普通不过的代码:

csharp 复制代码
var ids = new List<int> { 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 常量

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 不再内联常量,而是通过 OPENJSONCTE(公用表表达式) 来传递参数化集合。简化后的生成逻辑是:

复制代码
简单值列表(string/int 常量)→ OPENJSON 方式
复杂查询 / 多次 Contains → CTE(WITH ... AS)方式

对于 ids.Contains(x.id) 这类场景,EF 可能生成类似这样的 SQL:

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):

Contains in LINQ queries may stop working on older SQL Server versions

Impact: High

链接:learn.microsoft.com - Breaking changes in EF Core 8.0

3. 什么时候会触发?

不是所有 Contains 都会炸,但它会在你不经意间冒出来。触发条件包括但不限于:

场景 风险
ids.Contains(x.id)idsList<int> 🔴 高
ids.Contains(x.id)idsList<int?> 🔴 高(我们项目遇到的)
stringList.Contains(x.name) 🟡 中(可能走 OPENJSON)
同一查询中有多个 Contains 🔴 高
.Contains() 嵌套在复杂 Where 表达式中 🟡 中
查询中同时有其他关联(Join / Include) 🔴 高

最重要的信号 :一旦看到错误信息里出现 WITH分号,99% 就是这个问题。

4. 解决方案(5 种,从优到差)

方案一:参数化 Raw SQL(推荐 ⭐)

直接绕过 EF 翻译,用 FromSqlRaw + SqlParameter,性能最优,零坑:

csharp 复制代码
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();
  • ✅ 生成的 SQL 就是简单的 WHERE id IN (@p0, @p1, ...)
  • Microsoft.Data.SqlClient 随 EF Core SQL Server 包引入,无需另装
  • ✅ 完全防注入
  • ⚠️ 需要知道表名(但你的 DbContext 本来就定义了)

适用:批量删除、批量更新、批量查询等「已知 ID 列表查实体」场景。

方案二:FindAsync 逐个查询(小数据量 ⭐)

如果 ID 列表很短(比如页面批量操作选 10-20 条),直接用主键查:

csharp 复制代码
var users = new List<sys_admin>();
foreach (var id in ids)
{
    var user = await _db.sys_admin.FindAsync(id);
    if (user != null) users.Add(user);
}
  • FindAsync 走主键索引直查,不生成 CTE
  • ✅ 简单可靠
  • ❌ N+1 查询,ID 数量多时性能差

适用:后台管理的批量操作(用户勾选几条记录删除/启用等),ID 数量通常不超过几十个。

方案三:全量拉到内存过滤(小表 ⭐)

csharp 复制代码
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> 换成 int[] 后,EF 生成的 SQL 就不同了:

csharp 复制代码
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,可以用新增的配置项恢复旧行为:

csharp 复制代码
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(connectionString)
        .TranslateParameterizedCollectionsToConstants();
}

或者:

csharp 复制代码
builder.Services.AddDbContext<MyDbContext>(options =>
    options.UseSqlServer(connectionString,
        sqlOptions => sqlOptions.TranslateParameterizedCollectionsToConstants()));
  • ✅ 全局生效,一行配置解决
  • ✅ 恢复 EF Core 7 的 Contains 翻译方式
  • ❌ 回到旧行为:查询计划缓存问题依旧存在(微软当初改它的原因)
  • ❌ 需要 EF Core 9+

5. 推荐策略

复制代码
你的 SQL Server 版本 >= 2016 且兼容级别 >= 130?
  └─ 是 → 方案一(FromSqlRaw)或方案二(FindAsync),保持 EF8 的新行为
  └─ 否 → 考虑升级 SQL Server 或兼容级别

你的项目还在 .NET 8 / EF Core 8?
  └─ 批量操作(已知 IDs)→ 方案一 或 方案二
  └─ 动态查询(用户输入过滤)→ 直接用 EF8 的 OPENJSON 方式不会触发 CTE 问题
  └─ 小表兜底 → 方案三

你的项目已升级到 EF Core 9?
  └─ 考虑方案五,但要理解它的代价(查询计划缓存退化)

6. 实战案例:MiePcb 项目经验

我们在 MiePcb 管理后台项目中先后踩了两次这个坑:

踩坑 1:GetPageList 的部门查询

csharp 复制代码
// 错误写法
query = query.Where(x => deptIds.Contains(x.dept_id));

报错WITH 附近语法错误。

修复 :内存全量拉取部门表,再 Join 过滤。

csharp 复制代码
var depts = await _db.sys_dept.Select(d => new { d.id, d.name }).ToListAsync();
// 后续在内存中关联

踩坑 2:BatchDelete 的批量删除

csharp 复制代码
// 错误写法
var users = await _db.sys_admin.Where(x => ids.Contains(x.id)).ToListAsync();

报错:同上。

修复 :方案一(FromSqlRaw + 参数化),一次查询干净利落。

经验总结

  1. EF Core 8 的项目,所有 Contains 都要在心里打个问号 --- 写的时候就要想好万一报错走哪个方案
  2. List<int?>List<int> 更容易触发 --- 可空类型让 EF 生成的 SQL 更复杂,更倾向 CTE
  3. SQL Server 错误 156 看到 WITH 就看 Contains --- 排查方向比错误信息本身更重要
  4. 记录到 MEMORY.md --- 团队其他人可能不知道这个坑,要有文档沉淀

一句话总结 :EF Core 8 把 Contains 翻译从"内联常量"改成了"CTE / OPENJSON",CTE 要求前面有分号但 EF 没补,SQL Server 就炸了。修起来简单------用 FromSqlRaw 参数化查询、FindAsync、或者内存过滤。核心原则:不再盲目信任 EF 的 Contains 翻译,批量操作优先 Raw SQL。

我是刚子,一个还在写 .NET 代码的程序员。如果觉得文章内容还可以,记得一键三连哦,欢迎点赞、收藏、转发给更多的小伙伴。咱们下回见!

阅读原文:EF Core 8 + SQL Server:Contains() 突然报 "关键字 WITH 附近有语法错误"?一篇避坑指南 - 码农刚子的开发笔记

相关推荐
todoitbo1 个月前
CTE 与外层 JOIN 的条件下推:原理、边界与验证方法
数据库·join·cte
l1t5 个月前
DeepSeek对利用DuckDB求解Advent of Code 2021第9题“烟雾盆地”第二部分SQL的分析
数据库·人工智能·sql·递归·duckdb·deepseek·cte
l1t5 个月前
postgresql递归查询指定搜索顺序的方法
数据库·postgresql·dfs·递归·cte
l1t5 个月前
sqlite递归查询指定搜索顺序的方法
数据库·sql·sqlite·dfs·递归·cte
l1t6 个月前
在duckdb 递归CTE中实现深度优先搜索DFS
sql·算法·深度优先·duckdb·cte
狂盗一枝梅7 个月前
MySql8.0公共表表达式『CTE』
mysql·cte
在未来等你1 年前
SQL进阶之旅 Day 13:CTE与递归查询技术
sql·数据分析·数据库开发·sql优化·递归查询·cte
kngines1 年前
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】1.4 数据库与表的基本操作(DDL/DML语句)
数据库·postgresql·数据分析·cte·age
kngines1 年前
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】4.2 数据类型转换(CAST函数/自定义函数)
数据库·postgresql·数据分析·filter·自定义函数·cte