序列化 JSON 时崩了?99% 是 EF 延迟加载惹的祸,三种解法拿走不谢

序列化 JSON 时崩了?99% 是 EF 延迟加载惹的祸,三种解法拿走不谢
一、先看一个“翻车”现场假设我们有这么三层数据主表fin_voucher_rule_master凭证规则明细表fin_voucher_rule_detail规则明细条件表fin_voucher_rule_condition明细下的条件关系很简单一个主表有多个明细一个明细有多个条件。DAL 层数据访问层代码public static Listfin_voucher_rule_master GetFin_Voucher_Rule_Masters(string name) { using (var db new PcbEntities()) { var q db.fin_voucher_rule_master.AsNoTracking(); if (!string.IsNullOrEmpty(name)) q q.Where(x x.business_type.Contains(name)); return q.ToList(); } }服务层代码转成 JSON 返回给前端public static string GetFin_Voucher_Rule_Masters(string name) { var list DAL.VoucherRuleMasterDAL.GetFin_Voucher_Rule_Masters(name); string json ZhPcb.Common._Json.GetJsonfin_voucher_rule_master(list); json {\total\: list.Count ,\rows\: json }; return json; }看着没啥问题吧结果一跑崩了System.ObjectDisposedException: 此 ObjectContext 实例已释放不可再用于需要连接的操作。刚子第一次遇到的时候也是一脸懵我明明ToList()了数据都查出来了怎么还说“上下文已释放”二、原因分析都是“延迟加载”惹的祸啥是延迟加载简单说EF 默认开启了一个“懒汉模式”你查主表的时候它只把主表自己的字段比如 Id、Name给你。至于主表关联的明细fin_voucher_rule_detail它先不查等你真正用到的时候它再偷偷去数据库查。这个“偷偷查”的动作就叫延迟加载。为啥ToList()之后还会查数据库很多人以为ToList()就万事大吉了数据全在内存里了。错ToList()只加载了你明确查询的那些字段。你没有Include的导航属性EF 不会主动加载。等你一访问它比如 JSON 序列化器想把它转成字符串EF 才反应过来“哎呀这个还没查呢我赶紧去数据库拿。”——结果发现DbContext已经在using外面被释放了于是报错。本案例的执行流程拆解版DAL 层using (var db new PcbEntities())创建数据库连接。执行.AsNoTracking().Where(...).ToList()只加载了主表自身的字段。using块结束db被释放连接关闭。服务层调用 JSON 序列化工具。序列化器很“勤奋”它会递归遍历master对象的所有属性包括导航属性比如fin_voucher_rule_detail。当序列化器访问master.fin_voucher_rule_detail时EF 的代理对象尝试延迟加载——但DbContext已经没了于是抛出ObjectDisposedException。划重点你让 EF 帮你查主表它只给了你主表。你以为完事了结果序列化的时候非要去看明细EF 说“那我再去数据库拿”可数据库连接早就关了。不崩你崩谁三、解决方案三种按需选方案一关掉延迟加载最简单在DbContext里把延迟加载功能关掉。这样 EF 就不会“偷偷”查数据库了访问导航属性只会得到null或空集合不会报错。using (var db new PcbEntities()) { db.Configuration.LazyLoadingEnabled false; // EF6 写法 // 如果是 EF Coredb.ChangeTracker.LazyLoadingEnabled false; var q db.fin_voucher_rule_master.AsNoTracking(); // ... return q.ToList(); }优点改一行代码搞定省心。缺点如果业务上确实需要明细数据你得自己Include。划重点Web 应用一般推荐全局关掉延迟加载然后在需要的地方手动Include。这样最可控。方案二提前加载导航属性Eager Loading用Include和ThenInclude告诉 EF“把明细和条件也一起查出来别偷懒。”EF Core 写法推荐using Microsoft.EntityFrameworkCore; public static Listfin_voucher_rule_master GetFin_Voucher_Rule_Masters(string name) { using (var db new PcbEntities()) { var q db.fin_voucher_rule_master .Include(m m.fin_voucher_rule_detail) .ThenInclude(d d.fin_voucher_rule_condition) .AsNoTracking(); if (!string.IsNullOrEmpty(name)) q q.Where(x x.business_type.Contains(name)); return q.ToList(); } }EF6 写法不支持ThenInclude用字符串或Selectusing System.Data.Entity; public static Listfin_voucher_rule_master GetFin_Voucher_Rule_Masters(string name) { using (var db new PcbEntities()) { var q db.fin_voucher_rule_master .Include(fin_voucher_rule_detail.fin_voucher_rule_condition) .AsNoTracking(); // 或者用 lambda.Include(x x.fin_voucher_rule_detail.Select(d d.fin_voucher_rule_condition)) if (!string.IsNullOrEmpty(name)) q q.Where(x x.business_type.Contains(name)); return q.ToList(); } }优点一次查询把所有需要的数据都拿到性能好。缺点得提前知道要加载哪些导航属性。划重点Include就是“带上明细”ThenInclude就是“再带上明细下面的条件”。EF Core 的写法更直观EF6 稍微别扭点但也能用。方案三用 DTO 投影最专业不直接返回实体而是建一个数据传输对象DTO只放你需要返回的字段。查询的时候直接投影彻底绕过导航属性。public class VoucherRuleMasterDto { public int Id { get; set; } public string BusinessType { get; set; } public ListVoucherRuleDetailDto Details { get; set; } } public static ListVoucherRuleMasterDto GetVoucherRuleMasters(string name) { using (var db new PcbEntities()) { var query db.fin_voucher_rule_master.AsNoTracking(); if (!string.IsNullOrEmpty(name)) query query.Where(x x.business_type.Contains(name)); return query.Select(m new VoucherRuleMasterDto { Id m.Id, BusinessType m.business_type, Details m.fin_voucher_rule_detail.Select(d new VoucherRuleDetailDto { Id d.Id, Condition d.fin_voucher_rule_condition.Description }).ToList() }).ToList(); } }优点只返回前端真正需要的数据网络传输量小。彻底解耦数据库模型和 API 接口。再也不用担心延迟加载和序列化问题。缺点多写几个 DTO 类可以用 AutoMapper 偷懒。划重点DTO 方案是最“正统”的做法推荐在正式项目里用。实体是给数据库用的DTO 是给前端用的别混在一起。四、EF6 和 EF Core 的 Include/ThenInclude 区别一张表看懂很多从 EF6 转到 EF Core 的兄弟会被ThenInclude搞晕。刚子给你总结一下你要加载什么EF6 写法EF Core 写法单个导航属性如 Detail.Include(x x.Detail)或Include(Detail).Include(x x.Detail)嵌套属性Detail.Condition.Include(x x.Detail.Condition)或Include(Detail.Condition).Include(x x.Detail).ThenInclude(d d.Condition)集合的嵌套属性Details里的Condition.Include(x x.Details.Select(d d.Condition)).Include(x x.Details).ThenInclude(d d.Condition)常见坑编译错误Include does not contain a definition for ThenInclude→ 说明你用的是 EF6别用ThenInclude。运行时InvalidOperationException: A second operation started on this context before a previous operation completed→ 你在遍历结果时又触发了延迟加载关掉它或者提前Include。五、最佳实践总结照着做不踩坑Web 应用全局关闭延迟加载在DbContext构造函数里加上public PcbEntities() { this.Configuration.LazyLoadingEnabled false; // EF6 // 或 this.ChangeTracker.LazyLoadingEnabled false; // EF Core }需要关联数据的地方手动Include。只读查询一律用AsNoTracking()提升性能减少内存占用。别在using块外面访问导航属性要么提前Include要么把序列化操作放在using里面。优先用 DTO 投影这是最干净、最专业的做法。搞清楚你的 EF 版本看packages.config或.csprojEntityFramework6.x → EF6Microsoft.EntityFrameworkCore→ EF Core六、完整示例拿来即用以下是一个兼容 EF6 和 EF Core 的写法通过条件编译public static Listfin_voucher_rule_master GetFin_Voucher_Rule_Masters(string name) { using (var db new PcbEntities()) { // 关闭延迟加载 db.Configuration.LazyLoadingEnabled false; var q db.fin_voucher_rule_master.AsNoTracking(); // 预先加载三层数据 #if EF6 q q.Include(fin_voucher_rule_detail.fin_voucher_rule_condition); #else q q.Include(m m.fin_voucher_rule_detail) .ThenInclude(d d.fin_voucher_rule_condition); #endif if (!string.IsNullOrEmpty(name)) q q.Where(x x.business_type.Contains(name)); return q.ToList(); } }最后刚子想说ObjectDisposedException这个错基本每个用 EF 的人都遇到过。搞懂了延迟加载的原理解决起来其实很简单。记住一句话在 DbContext 还活着的时候把你需要的数据全都显式加载好。如果你觉得这篇文章帮你搞明白了点个赞、转给还在被 EF 坑的兄弟。