大家好,我是刚子。

你是不是也遇到过这种报错:ObjectDisposedException: 此 ObjectContext 实例已释放,不可再用于需要连接的操作

明明数据都查出来了,为啥序列化成 JSON 的时候突然崩了?今天刚子就用一个真实案例,把这个坑给你讲明白,顺便把 IncludeThenInclude、延迟加载这些概念一次性捋清楚。


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

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

  • 主表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

    • EntityFramework 6.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 坑的兄弟

我是刚子,一个还在写 .NET 的老程序员。咱们下回见!