当前位置: 首页 > news >正文

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

大家好,我是码农刚子,最近遇到一个EF Core 查询的问题,想跟大家分享下,应该有很多小伙伴已经遇到过了。

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

1. 先看症状

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

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 常量

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

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

链接: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,性能最优,零坑:

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 条),直接用主键查:

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 数量通常不超过几十个。

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

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 就不同了:

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<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的部门查询

// 错误写法 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(); // 后续在内存中关联

踩坑 2:BatchDelete的批量删除

// 错误写法 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 附近有语法错误"?一篇避坑指南 - 码农刚子的开发笔记

http://www.jsqmd.com/news/979865/

相关文章:

  • C# WinForm版开心消消乐完整工程:含源码、资源、存档与SQLite支持
  • 绵阳高新区卖黄金注意事项 靠谱回收门店推荐 - 润富黄金回收
  • BetterNCM插件管理器:3分钟搞定网易云音乐插件安装的终极方案
  • 白银市黄金回收+白银回收+铂金回收+彩金回推荐收门店 本地靠谱店铺指南及地联系方式址和 - 大熊猫898989
  • 英语六级真题备考最全攻略|刷题技巧及提分方法
  • 《代码整洁之道》——读书笔记(持续更新)
  • 百色市黄金回收本地靠谱店铺指南+白银回收+铂金回收+彩金回推荐收门店 及地联系方式址推荐 - 盛世金银回收
  • 从爬虫到官方导出:我的4000张语义分割数据‘解救’之路与飞桨EasyDL更新评测
  • AGI五年概率背后的四大技术支点与工程落地路径
  • 2026年Q2长沙K金回收技术要点及靠谱渠道解析:长沙钻石回收/长沙铂金回收/长沙银元回收/长沙首饰回收/长沙高档礼品回收/选择指南 - 优质品牌商家
  • C# WinForm 与 VP 二次开发
  • Python 3.9核心升级解析:GenericAlias、字典合并与zoneinfo迁移指南
  • 物业安保对讲机怎么选?稳定易用高性价比
  • 百色市黄金回收+白银回收+铂金回收+彩金回推荐收门店 本地靠谱店铺指南及地联系方式址和 - 大熊猫898989
  • sqli-labs解题思路(Less-12到Less-22)
  • ▲基于Qlearning强化学习的地下矿井OFDM自适应调制通信系统matlab仿真
  • 2026年度静压式液位计优选品牌TOP10 | 国产替代进程下的技术突围与实战选型指南 - 仪表品牌榜
  • 蚌埠市黄金回收本地靠谱店铺指南+白银回收+铂金回收+彩金回推荐收门店 及地联系方式址推荐 - 盛世金银回收
  • Kimai:开源时间追踪,个人到企业都能用
  • Zotero-Style:3个颠覆性改变如何重构你的文献管理方法论
  • DDPG训练总崩?TD3的三个‘延迟’技巧如何让你的智能体更稳定(附调参心得)
  • 绵阳游仙区黄金回收哪家靠谱 盘点正规回收门店 - 润富黄金回收
  • 从CenterPoint看3D目标检测演进:为什么“点”比“框”更适合自动驾驶?
  • 基于TCN-Transformer-BiGRU多输出回归+SHAP可解释性分析+NSGAII Matlab代码(三目标)
  • 2026年石家庄空调移机选对=省心 大为搬家推荐 - 本地品牌推荐
  • TanStack 2026 全景:从“阮一峰推荐的好用库“到“Next.js 真正的对手“
  • 2026通讯行业高效交付触控面板供应商推荐:丝印面板/亚克力触控面板/亚克力面板/半透面板/印刷面板/喷涂面板/选择指南 - 优质品牌商家
  • 电商与AI智能客服场景下的Java大厂面试:从Spring微服务到RAG智能客服的实战拷问
  • 蚌埠市黄金回收+白银回收+铂金回收+彩金回推荐收门店 本地靠谱店铺指南及地联系方式址和 - 大熊猫898989
  • 模型无关AI系统:构建可演进的AI服务契约体系