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

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

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。

  1. 根因: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):

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

Impact: High

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

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

场景 风险
ids.Contains(x.id) 且 ids 是 List 🔴 高
ids.Contains(x.id) 且 ids 是 List<int?> 🔴 高(我们项目遇到的)
stringList.Contains(x.name) 🟡 中(可能走 OPENJSON)
同一查询中有多个 Contains 🔴 高
.Contains() 嵌套在复杂 Where 表达式中 🟡 中
查询中同时有其他关联(Join / Include) 🔴 高
最重要的信号:一旦看到错误信息里出现 WITH 和 分号,99% 就是这个问题。

  1. 解决方案(5 种,从优到差)
    方案一:参数化 Raw SQL(推荐 ⭐)
    直接绕过 EF 翻译,用 FromSqlRaw + SqlParameter,性能最优,零坑:

var paramNames = ids.Select((_, i) =>"@pi").ToArray();varparameters=ids.Select((id,i)=>newMicrosoft.Data.SqlClient.SqlParameter("@p{i}").ToArray(); var parameters = ids.Select((id, i) => new Microsoft.Data.SqlClient.SqlParameter("@pi").ToArray();varparameters=ids.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 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[] 后,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 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 + 参数化),一次查询干净利落。

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

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

相关文章:

  • 万国官方售后服务中心服务电话及完整地址实地考察报告_多信源验证(2026年7月更新) - 万国中国官方服务中心
  • WarcraftHelper魔兽辅助工具:3分钟快速配置,让你的经典魔兽在现代电脑上完美运行
  • C/C++ assert 宏实战:5个典型场景与3个常见陷阱(附NDEBUG开关)
  • 企业级Agent能否处理长链路复杂任务?:深度拆解工程化落地路径与主流方案技术架构
  • SSH私钥权限问题:chmod 0600原理与跨平台安全配置详解
  • 2026年西安碑林商务出行旅游安排盘点实用信息大集合
  • markdown:分页,另起一页
  • UE4SS Hook机制深度解析:从注册到注销的稳定Mod开发实践
  • STM32 HAL 库 vs LL 库 vs 寄存器开发:3 种方式在 GPIO 控制上的 50% 性能差异实测
  • Grok上车后车载数据隐私风险与管控实战指南
  • 2026年7月最新绍兴帝舵官方售后客服中心地址电话及服务网点分布 - 帝舵中国官方服务中心
  • 深度解析意大利现阶段经济发展行情
  • Rust开发5MB轻量级Markdown阅读器:多标签与实时编辑实践
  • Mainline 作者们:聊聊从代码协作到意图协作
  • HTTPS证书信任链全解析:JMeter根证书导入与浏览器信任配置实战
  • Meta Watermelon模型追平GPT-5.5:AI大模型技术突破与开源影响分析
  • 2026年7月最新海口爱彼官方售后客服服务电话及地址网点大全 - 爱彼中国官方服务中心
  • Codex CLI 四维配置体系:环境、配置、协议与语义深度解析
  • Python 3.12 + OpenCV 4.8 游戏自动化脚本:5步实现后台键鼠与图色识别
  • 变增益运放设计:从原理到实践,解决信号动态范围问题
  • P3366 【模板】最小生成树
  • 亨得利官方名表服务中心|官方电话和详细网点地址权威信息声明(2026年7月最新) - 亨得利官方博客
  • 亨得利官方名表服务中心|网点地址与售后电话权威信息声明(2026年7月最新) - 亨得利钟表维修中心
  • 丝杆升降机通讯故障,Profibus/CANopen/EtherCAT排查
  • 医院陪护系统源码如何帮助机构建立数字化陪护平台?
  • 节省Token的方案-现在 GitHub 上比较成型的开源方案,重点看“上下文压缩、仓库索引/RAG、代码库摘要、AI 规则/记忆管理”这几类
  • 2026年7月最新宝珀上海临港海港中心万象汇维修保养服务电话 - 宝珀官方售后服务中心
  • 避坑指南,微调、RAG、prompt(提示词)的落地选型误区,90%的AI新人都踩过
  • Midjourney 垫图、参考图怎么用?让生成更贴近想要的效果
  • LangChain 0.3.x 实战:5行代码构建RAG问答链,集成Qwen2-7B本地模型