EF Lazy Loading Breaks JSON Serialization: Root Cause and 3 Reliable Fixes

This article focuses on a common scenario where Entity Framework triggers ObjectDisposedException during JSON serialization. The core issue is that lazy loading accesses navigation properties after the DbContext has already been disposed. Practical solutions include disabling lazy loading, explicitly eager loading related data, and projecting to DTOs. Keywords: EF Core, lazy loading, JSON serialization.

Technical Specification Snapshot

Parameter Description
Tech stack C# / .NET / Entity Framework 6 / EF Core
Core issue Lazy loading is triggered during JSON serialization, causing a disposed context exception
Typical exception ObjectDisposedException
Query strategies AsNoTracking, Include, ThenInclude, Select projection
Applicable scenarios Web API, MVC, service layers returning JSON
Article type Hands-on troubleshooting blog post with summarized guidance

WeChat Official Account QR Code AI Visual Insight: This image shows a QR code that links to the author’s technical community. It is primarily used for developer follow-up and content distribution. It does not contain code structure, architectural relationships, or runtime output, so it is not part of the core implementation details.

This exception happens because the object graph gets “filled in” during serialization

In many projects, the DAL has already executed ToList(), yet JSON serialization in the service layer still throws ObjectDisposedException. That does not mean the root entity query failed. It means the navigation properties were never fully loaded.

EF uses lazy loading by default. In other words, related properties on an entity may still be proxy objects. When the serializer recursively accesses those properties, EF attempts another database query.

You can reproduce the failure with a minimal example

public static List
<fin_voucher_rule_master> GetMasters(string name)
{
    using (var db = new PcbEntities())
    {
        var query = db.fin_voucher_rule_master.AsNoTracking(); // Read-only query with tracking disabled

        if (!string.IsNullOrEmpty(name))
            query = query.Where(x => x.business_type.Contains(name)); // Apply conditional filter

        return query.ToList(); // This only materializes fields from the main table
    } // DbContext is disposed here
}

This code only queries the main entity. It does not explicitly load the detail or condition collections.

public static string GetMastersJson(string name)
{
    var list = GetMasters(name);
    return System.Text.Json.JsonSerializer.Serialize(list); // Serializer recursively accesses navigation properties
}

This is the step that actually triggers the problem: the serializer touches navigation properties, EF tries to load more data, but the context is already gone.

The root cause is not ToList(), but navigation properties that were never explicitly loaded

ToList() only guarantees that the current LINQ query result is materialized. It does not guarantee that all related objects are fully loaded into memory. Without Include, navigation properties may still remain unloaded.

This is where many developers misdiagnose the issue: it looks like “the data has already been queried,” but in reality only the main table was loaded, while related objects still depend on a live context.

The execution flow can be summarized in four steps

// 1. Query the main table
var list = db.fin_voucher_rule_master.AsNoTracking().ToList();

// 2. Dispose the context
// using block ends, database connection is closed

// 3. The serializer starts traversing entity properties
// 4. Accessing navigation properties triggers EF lazy loading and eventually throws an exception

This sequence shows that the exception occurs during serialization, but the root cause originates at query time.

Disabling lazy loading is the most direct defensive strategy

If your system is a Web API or MVC application, disabling lazy loading globally is usually the safest recommendation. That prevents query behavior from happening at unpredictable times, especially when you return entities directly to the frontend.

EF6 and EF Core disable lazy loading differently

using (var db = new PcbEntities())
{
    db.Configuration.LazyLoadingEnabled = false; // Disable lazy loading in EF6

    var data = db.fin_voucher_rule_master
        .AsNoTracking() // Recommended for read-only scenarios
        .ToList();
}

This code prevents EF6 from automatically going back to the database when navigation properties are accessed.

public PcbEntities()
{
    this.Configuration.LazyLoadingEnabled = false; // Disable globally in EF6
    // If EF Core lazy-loading proxies are enabled, disable that capability in the configuration layer instead
}

Once disabled globally, all related data must be declared explicitly, which makes system behavior much more predictable.

Explicit eager loading is the most common and stable query strategy

If the business logic really needs three levels of data—master, detail, and condition—the right approach is not to rely on lazy loading. Instead, load the full graph up front in a single query.

EF Core should use Include and ThenInclude

using Microsoft.EntityFrameworkCore;

var list = db.fin_voucher_rule_master
    .Include(m => m.fin_voucher_rule_detail) // Eager load the detail collection
    .ThenInclude(d => d.fin_voucher_rule_condition) // Continue loading the condition collection
    .AsNoTracking()
    .ToList();

This code loads the complete object graph before the context is disposed, so serialization no longer requires extra queries.

EF6 requires a string path or nested Select

using System.Data.Entity;

var list = db.fin_voucher_rule_master
    .Include("fin_voucher_rule_detail.fin_voucher_rule_condition") // Eager load multi-level navigation in EF6
    .AsNoTracking()
    .ToList();

This achieves the same goal as the EF Core version. Only the API style is different.

DTO projection is the long-term solution for interface-oriented design

Returning EF entities directly can easily introduce circular references, unnecessary field exposure, and unpredictable serialization behavior. A more professional approach is to project query results into DTOs and return those to the frontend.

Projection queries eliminate lazy loading side effects at the source

public class VoucherRuleMasterDto
{
    public int Id { get; set; }
    public string BusinessType { get; set; }
    public List
<VoucherRuleDetailDto> Details { get; set; }
}

public class VoucherRuleDetailDto
{
    public int Id { get; set; }
    public string Condition { get; set; }
}

var result = db.fin_voucher_rule_master
    .AsNoTracking()
    .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 // Only project fields required by the frontend
        }).ToList()
    })
    .ToList();

This code cleanly separates the database model from the API model, which makes it a strong fit for long-term maintenance in production systems.

EF6 and EF Core have clear differences in loading syntax

Goal EF6 EF Core
Single-level navigation .Include(x => x.Detail) .Include(x => x.Detail)
Multi-level reference navigation Include("Detail.Condition") .Include(x => x.Detail).ThenInclude(d => d.Condition)
Child navigation under a collection .Include(x => x.Details.Select(d => d.Condition)) .Include(x => x.Details).ThenInclude(d => d.Condition)

If ThenInclude is not recognized, the project is usually using EF6 rather than EF Core.

A safer combined pattern looks like this

public static List
<fin_voucher_rule_master> GetSafeData(string name)
{
    using (var db = new PcbEntities())
    {
        db.Configuration.LazyLoadingEnabled = false; // Disable lazy loading first

        var query = db.fin_voucher_rule_master
            .Include("fin_voucher_rule_detail.fin_voucher_rule_condition") // Then explicitly load the required data
            .AsNoTracking();

        if (!string.IsNullOrEmpty(name))
            query = query.Where(x => x.business_type.Contains(name)); // Preserve the filter condition

        return query.ToList();
    }
}

This approach works well for most traditional EF6 web projects. It is stable, straightforward, and easy to audit.

Engineering practice should prioritize predictable data boundaries

In web applications, the best rule is simple: explicitly prepare all data that the frontend needs while the DbContext is still alive. Do not hand EF proxy objects directly to the serializer and hope for the best.

For read-only endpoints, prefer AsNoTracking(). For public APIs, prefer DTOs. For complex list pages, define Include paths explicitly and keep field size under control.

FAQ

Q1: Why does serialization still hit the database even after ToList()?

Because ToList() only materializes the entities selected by the current query. It does not automatically load navigation properties that were not included with Include. When the serializer reaches those properties, EF attempts lazy loading.

Q2: Why do navigation properties become empty or null after disabling lazy loading?

Because once automatic database round-trips are disabled, EF will no longer silently fetch related data. If you need related objects, you must load them explicitly with Include, ThenInclude, or DTO projection.

Q3: Which solution should I prioritize in production?

For an immediate fix, disable lazy loading and add the required Include paths. For medium- and long-term architecture, DTO projection is the best practice. It reduces field exposure, improves API stability, and avoids serialization side effects.

Core summary: This article systematically breaks down why Entity Framework throws ObjectDisposedException during JSON serialization: lazy loading still tries to access navigation properties after the DbContext has been disposed. It presents three solutions—disabling lazy loading, eager loading with Include/ThenInclude, and DTO projection—while also summarizing syntax differences between EF6 and EF Core, best practices, and a practical troubleshooting path.