Skip to main content

Command Palette

Search for a command to run...

ObjectDisposedException: Cannot Access a Disposed Context Instance in ASP.NET Core โ€” Root Cause and Fix

Updated
โ€ข12 min read
ObjectDisposedException: Cannot Access a Disposed Context Instance in ASP.NET Core โ€” Root Cause and Fix

The error ObjectDisposedException: Cannot access a disposed context instance is one of those production surprises that hits developers at the worst time โ€” usually under load, during a scheduled job, or right after a seemingly harmless refactor. Understanding why EF Core's DbContext gets disposed before you're done with it, and how to stop it from happening again, is what this article is about.

When this exception surfaces, the full message typically reads: "Cannot access a disposed context instance. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application." That message from EF Core is accurate โ€” but it covers only the most obvious scenario. Production codebases hit this error in at least five distinct patterns, each with its own fix. For developers who want to see these patterns in a complete, production-wired codebase, the full implementations with edge cases and lifecycle diagrams are available on Patreon โ€” ready to run and adapt.

Understanding EF Core's DbContext in context of dependency injection lifetimes is essential before any fix makes sense. Seeing how it fits into a real production API โ€” alongside background jobs, scoped repositories, and hosted services โ€” is covered in detail across Chapters 3 and 12 of the Zero to Production course, which walks through the full lifecycle inside a running codebase.

ASP.NET Core Web API: Zero to Production

What Does This Error Actually Mean?

ObjectDisposedException on a DbContext means the instance has been disposed โ€” its underlying database connection has been returned, its internal state has been cleared โ€” and your code then attempted to use it for a query or save.

EF Core registers DbContext as a scoped service by default. In ASP.NET Core, a scope corresponds to a single HTTP request. When the request ends, the DI container disposes everything registered as scoped, including your DbContext. If any code holds a reference to that context and tries to use it after that disposal point, the exception fires.

The error manifests in multiple forms:

  • System.ObjectDisposedException: Cannot access a disposed context instance

  • ObjectDisposedException: Cannot access a disposed object

  • ObjectDisposedException: 'ApplicationDbContext'

All three point to the same root cause: something outlived the scope in which the DbContext was created.

The Five Root Causes (and Their Fixes)

Cause 1: Injecting a Scoped DbContext Into a Singleton

This is the most common cause. When a singleton service โ€” which lives for the entire application lifetime โ€” receives a scoped DbContext through constructor injection, the context gets captured inside the singleton. The first request creates the singleton, the DI container injects a DbContext tied to that request's scope, and when that request ends, the context is disposed. Every subsequent request that calls the singleton then hits a disposed context.

ASP.NET Core's service validation catches this in development: it will throw a different error โ€” InvalidOperationException: Cannot resolve scoped service from root provider. But when validation is disabled or bypassed, it silently creates the disposed-context time bomb.

The fix is to never inject a DbContext (or any scoped service) directly into a singleton. Instead, inject IServiceScopeFactory and create a scope each time you need a context.

private readonly IServiceScopeFactory _scopeFactory;

public MyService(IServiceScopeFactory scopeFactory)
{
    _scopeFactory = scopeFactory;
}

public async Task DoWorkAsync()
{
    using var scope = _scopeFactory.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    // use db here โ€” it will be disposed when scope ends
}

If you are encountering the related Cannot resolve scoped service from root provider error, the dedicated production fix article covers that specific variant in depth.

Cause 2: Using DbContext in a BackgroundService Without Creating a Scope

BackgroundService and IHostedService implementations are registered as singletons. Injecting a DbContext directly into them triggers the same singleton-captures-scoped problem described above โ€” but this time, it may not surface immediately. The context appears to work during the first execution cycle. Once the first request scope that provided it ends, subsequent cycles throw the exception.

The correct pattern is to inject IServiceScopeFactory and create a new scope on every execution cycle or logical unit of work:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        await ProcessPendingItemsAsync(db, stoppingToken);
        await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
    }
}

This is also the correct fix for the background service memory leak pattern that stems from the same misuse of DI lifetimes in hosted services.

Cause 3: Wrapping DbContext in a using Statement Manually

This one catches developers who are thorough about resource cleanup but apply it incorrectly. When DbContext is resolved from DI, the DI container owns its lifecycle. Wrapping it in a using block in your code disposes it early โ€” before the request or scope has ended. Any code that then attempts to use the same instance (or a lazy navigation property loaded afterward) throws.

// Incorrect โ€” disposes context before the scope is done with it
using var context = _serviceProvider.GetRequiredService<ApplicationDbContext>();
var result = await context.Products.ToListAsync();
// context disposed here โ€” any lazy-loaded properties accessed after this will throw

The fix: do not manually dispose a DbContext that was resolved through DI. The container handles disposal when the scope ends. If you need fine-grained lifetime control, register a DbContextFactory (IDbContextFactory<TContext>) โ€” instances created through the factory are not managed by DI and must be disposed explicitly.

Cause 4: Fire-and-Forget Tasks Outliving the Request Scope

This is a subtle cause and frequently the hardest to diagnose in production. A controller action or service method fires a Task without await โ€” typically in an attempt to "not block the response." That task captures the DbContext from the current scope. When the request completes and returns the response, the scope disposes the context. The background task, still running, then tries to access the now-disposed context.

// Problematic โ€” context is captured in a task that outlives the request
_ = Task.Run(() => _context.AuditLogs.AddAsync(auditEntry)); // context will be disposed
return Ok(result);

The fix has two options:

  1. Await the task before returning the response โ€” ensures the work completes within the scope.

  2. If the work genuinely must be deferred, use IServiceScopeFactory inside the background task to create its own scope and DbContext instance, completely independent of the request's scope.

For long-running deferred work, a proper background job system โ€” Hangfire or System.Threading.Channels โ€” is the right tool. The EF Core "A Second Operation Was Started" fix article covers related concurrency anti-patterns.

Cause 5: async void Methods Returning Before the Work Completes

An async void method is not awaitable. When a method returns async void, the caller cannot know when the method has completed. If an async void method uses a DbContext, the calling scope may end (and dispose the context) while the async void method is still executing its awaited operations.

The fix is categorical: never use async void except for event handlers where the framework requires it. Every async method that uses a DbContext must return Task or Task<T> so callers can await it and keep the scope alive for the duration.

How to Diagnose the Root Cause in Production

When the exception hits in production, the stack trace is the first thing to read carefully. Look for:

  • The frame that calls into DbContext โ€” what service or class initiated the query or save? Is it a singleton? A background service?

  • The thread or task context โ€” is there a Task.Run, ThreadPool.QueueUserWorkItem, or fire-and-forget pattern anywhere in the call chain?

  • Whether the context was manually disposed โ€” search the codebase for using var context or .Dispose() calls on DbContext or DbContextFactory instances.

In ASP.NET Core development environments, enable service scope validation:

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

ValidateScopes = true will throw at request time when a scoped service is accessed from a singleton scope. ValidateOnBuild = true will throw at application startup for services that are structurally misconfigured. These two flags catch the majority of lifetime mismatches before they become production incidents.

How to Prevent It Recurring

Register DbContext Correctly

EF Core's AddDbContext<T> registers the context as scoped by default โ€” this is correct for most APIs. Do not change this to singleton. If you need to create DbContext instances on demand (for background tasks or parallel operations), register a DbContextFactory alongside the scoped context:

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

Factory-created instances are fully independent and must be disposed by the caller with a using statement.

Apply the Scope Factory Pattern Consistently

Establish a team-wide convention: any class registered as singleton or that runs outside the request pipeline โ€” background services, event handlers, middleware that creates child operations โ€” uses IServiceScopeFactory, not a directly injected DbContext.

Review DI Lifetime Decisions at Code Review

Add DI lifetime checks to your code review checklist. A singleton injecting a scoped dependency is almost always a bug. Tools like Microsoft.Extensions.DependencyInjection scope validation and architecture testing with ArchUnit.NET (or similar) can enforce this automatically.

For a thorough look at lifetime tradeoffs across the full DI system, the ASP.NET Core DI Lifetimes Enterprise Decision Guide covers when each lifetime fits, the misuse patterns, and how to catch violations early.

Use Structured Logging to Catch Disposal Early

Before the exception reaches production, structured logging can surface the warning. EF Core emits log entries at Debug and Warning levels for unusual context state. Combined with a centralized logging pipeline โ€” Serilog with its request context enrichers โ€” these warnings become actionable signals rather than silent failures.

โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

FAQ

What is the difference between ObjectDisposedException and InvalidOperationException: Cannot resolve scoped service from root provider?

Both errors stem from DI lifetime mismatches, but they fire at different points. InvalidOperationException: Cannot resolve scoped service from root provider fires at resolution time โ€” when a singleton tries to obtain a scoped service from the DI container and service validation is enabled. ObjectDisposedException fires at access time โ€” when code actually uses a context that has already been disposed, typically because validation was disabled or the disposal happened due to a fire-and-forget pattern rather than a lifetime mismatch caught at resolution. In practice, if you see the ObjectDisposedException, scope validation was not catching it, which means the disposal is happening dynamically (a task outliving a scope) rather than structurally (a singleton holding a scoped reference).

Is it ever safe to inject DbContext directly into a singleton?

No. DbContext is designed for use within a single unit of work โ€” a request, a transaction, or a discrete background operation. Injecting it into a singleton means a single instance is shared across all requests, which leads to concurrency issues (multiple threads accessing the same context), stale change tracking state, and the ObjectDisposedException when the scope ends. Always use IServiceScopeFactory in singletons that need database access.

Why does DbContextFactory avoid this problem?

IDbContextFactory<T> creates new, independent DbContext instances that are not registered in the DI scope. The factory itself is registered as a singleton โ€” only the factory, not the context instances it produces. Instances created via factory.CreateDbContext() are owned by the calling code and must be disposed explicitly. Because they are not tied to any DI scope, they cannot be prematurely disposed by the scope ending. This makes them the right choice for background services, parallel processing, and any code that needs to control the context lifetime explicitly.

Does ValidateScopes = true catch all forms of this exception?

It catches the structural form โ€” where a singleton has a scoped dependency injected via constructor. It does not catch dynamic forms, such as a Task.Run lambda capturing a context from an outer scope, or an async void method outliving the request. For those patterns, code review, logging, and load testing are the primary defences. ValidateOnBuild = true catches additional structural issues at startup, including services referencing other services with incompatible lifetimes.

Can I use DbContext across multiple requests if I register it as singleton?

No โ€” and doing so introduces two compounding problems. First, DbContext is not thread-safe; concurrent access from multiple requests to the same instance will cause data corruption and the "second operation started" error. Second, the change tracker accumulates state across all requests, leading to unexpected behaviour and memory growth. The scoped lifetime exists precisely to tie one context instance to one unit of work.

What happens if I use AddDbContextPool โ€” does that change the disposal behaviour?

AddDbContextPool<T> configures EF Core to pool and reuse DbContext instances across requests rather than creating and destroying them on every request. The instances are still treated as scoped from the DI perspective โ€” each request gets one instance exclusively. When the request scope ends, the context is reset and returned to the pool rather than truly disposed. The same lifetime rules apply: do not inject pooled contexts into singletons, do not manually dispose them, do not use them outside the request scope.

Is this the same bug as EF Core "A Second Operation Was Started on This Context"?

They are related but distinct. "A second operation was started" fires when two concurrent operations attempt to use the same, live context simultaneously โ€” it is a concurrency issue within a scope. ObjectDisposedException fires when code tries to access a context that has already been disposed โ€” it is a lifetime issue across scopes. Both are symptoms of treating DbContext as a shared, long-lived resource rather than a short-lived unit of work.

More from this blog

C

Coding Droplets

209 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.