一、先看一个“翻车”现场
假设我们有这么三层数据:
- 主表:
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外面被释放了,于是报错。
本案例的执行流程(拆解版)
- 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 Core:db.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 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。
五、最佳实践总结(照着做,不踩坑)
Web 应用全局关闭延迟加载
在DbContext构造函数里加上:public PcbEntities() { this.Configuration.LazyLoadingEnabled = false; // EF6 // 或 this.ChangeTracker.LazyLoadingEnabled = false; // EF Core }需要关联数据的地方,手动
Include。只读查询一律用
AsNoTracking()
提升性能,减少内存占用。别在
using块外面访问导航属性
要么提前Include,要么把序列化操作放在using里面。优先用 DTO + 投影
这是最干净、最专业的做法。搞清楚你的 EF 版本
看packages.config或.csproj:EntityFramework6.x → EF6Microsoft.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 坑的兄弟。