大家好,我是刚子。
你是不是也遇到过这种报错:ObjectDisposedException: 此 ObjectContext 实例已释放,不可再用于需要连接的操作?
明明数据都查出来了,为啥序列化成 JSON 的时候突然崩了?今天刚子就用一个真实案例,把这个坑给你讲明白,顺便把 Include、ThenInclude、延迟加载这些概念一次性捋清楚。
一、先看一个“翻车”现场
假设我们有这么三层数据:
- 主表:
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 坑的兄弟。
我是刚子,一个还在写 .NET 的老程序员。咱们下回见!