序列化 JSON 时崩了?99% 是 EF 延迟加载惹的祸,三种解法拿走不谢
2026/7/23 2:11:48 网站建设 项目流程

一、先看一个“翻车”现场

假设我们有这么三层数据:

  • 主表fin_voucher_rule_master(凭证规则)
  • 明细表fin_voucher_rule_detail(规则明细)
  • 条件表fin_voucher_rule_condition(明细下的条件)

关系很简单:一个主表有多个明细,一个明细有多个条件。

DAL 层(数据访问层)代码:

public static List<fin_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.GetJson<fin_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外面被释放了,于是报错。

本案例的执行流程(拆解版)

  1. DAL 层:using (var db = new PcbEntities())创建数据库连接。
  2. 执行.AsNoTracking().Where(...).ToList(),只加载了主表自身的字段。
  3. using块结束,db被释放,连接关闭。
  4. 服务层调用 JSON 序列化工具。序列化器很“勤奋”,它会递归遍历master对象的所有属性,包括导航属性(比如fin_voucher_rule_detail)。
  5. 当序列化器访问master.fin_voucher_rule_detail时,EF 的代理对象尝试延迟加载——但DbContext已经没了,于是抛出ObjectDisposedException

划重点:你让 EF 帮你查主表,它只给了你主表。你以为完事了,结果序列化的时候非要去看明细,EF 说“那我再去数据库拿”,可数据库连接早就关了。不崩你崩谁?


三、解决方案(三种,按需选)

方案一:关掉延迟加载(最简单)

DbContext里把延迟加载功能关掉。这样 EF 就不会“偷偷”查数据库了,访问导航属性只会得到null或空集合,不会报错。

using (var db = new PcbEntities()) { db.Configuration.LazyLoadingEnabled = false; // EF6 写法 // 如果是 EF Core:db.ChangeTracker.LazyLoadingEnabled = false; var q = db.fin_voucher_rule_master.AsNoTracking(); // ... return q.ToList(); }

优点:改一行代码搞定,省心。
缺点:如果业务上确实需要明细数据,你得自己Include

划重点:Web 应用一般推荐全局关掉延迟加载,然后在需要的地方手动Include。这样最可控。


方案二:提前加载导航属性(Eager Loading)

IncludeThenInclude告诉 EF:“把明细和条件也一起查出来,别偷懒。”

EF Core 写法(推荐):
using Microsoft.EntityFrameworkCore; public static List<fin_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,用字符串或Select):
using System.Data.Entity; public static List<fin_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 List<VoucherRuleDetailDto> Details { get; set; } } public static List<VoucherRuleMasterDto> 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

五、最佳实践总结(照着做,不踩坑)

  1. Web 应用全局关闭延迟加载
    DbContext构造函数里加上:

    public PcbEntities() { this.Configuration.LazyLoadingEnabled = false; // EF6 // 或 this.ChangeTracker.LazyLoadingEnabled = false; // EF Core }

    需要关联数据的地方,手动Include

  2. 只读查询一律用AsNoTracking()
    提升性能,减少内存占用。

  3. 别在using块外面访问导航属性
    要么提前Include,要么把序列化操作放在using里面。

  4. 优先用 DTO + 投影
    这是最干净、最专业的做法。

  5. 搞清楚你的 EF 版本
    packages.config.csproj

    • EntityFramework6.x → EF6
    • Microsoft.EntityFrameworkCore→ EF Core

六、完整示例(拿来即用)

以下是一个兼容 EF6 和 EF Core 的写法(通过条件编译):

public static List<fin_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 坑的兄弟

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询