asynchronous programming
context management
resource disposal
C# async
programming best practices

Asynchronous method with context dispose

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A disposed-context error in C# async code usually means an object such as DbContext outlived the scope that created it. The most common cause is starting asynchronous work that continues after the request or dependency-injection scope has already ended. Fixing it is mostly about lifetime design, not about adding random ConfigureAwait calls or retry logic.

Understand Why the Context Gets Disposed

In ASP.NET Core, a scoped dependency such as DbContext is usually created per request and disposed when the request scope ends. If a method starts work and does not await it before returning, the request may finish while the task still tries to use the context.

A simplified bad pattern looks like this:

csharp
1public Task<int> CountUsersLaterAsync()
2{
3    return Task.Run(async () =>
4    {
5        await Task.Delay(1000);
6        return await _db.Users.CountAsync();
7    });
8}

The task may still be running after the request scope has been torn down. At that point, _db is disposed and the failure shows up far away from the original mistake.

Await Scoped Work Before Returning

If the work belongs to the request, keep it inside the request lifetime and await it directly.

csharp
1using Microsoft.EntityFrameworkCore;
2
3public sealed class UserService
4{
5    private readonly AppDbContext _db;
6
7    public UserService(AppDbContext db)
8    {
9        _db = db;
10    }
11
12    public async Task<int> CountActiveUsersAsync(CancellationToken ct)
13    {
14        return await _db.Users
15            .Where(u => u.IsActive)
16            .CountAsync(ct);
17    }
18}

Then await it in the controller:

csharp
1[HttpGet("active-count")]
2public async Task<IActionResult> Get(CancellationToken ct)
3{
4    int total = await _service.CountActiveUsersAsync(ct);
5    return Ok(new { total });
6}

This keeps the query and the context inside the same scope, which is what scoped services are designed for.

Do Not Use Fire-and-Forget with Scoped Services

If work must continue after the request, do not capture request-scoped services and hope they survive. Create a new scope inside a background worker instead.

csharp
1using Microsoft.Extensions.DependencyInjection;
2
3public sealed class ReportJob
4{
5    private readonly IServiceScopeFactory _scopeFactory;
6
7    public ReportJob(IServiceScopeFactory scopeFactory)
8    {
9        _scopeFactory = scopeFactory;
10    }
11
12    public async Task RunAsync(CancellationToken ct)
13    {
14        using var scope = _scopeFactory.CreateScope();
15        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
16
17        int count = await db.Users.CountAsync(ct);
18        Console.WriteLine($"users: {count}");
19    }
20}

This gives the background task its own lifetime boundary instead of borrowing the request's boundary.

Use IDbContextFactory When It Fits the Design

For code that needs to create contexts on demand, IDbContextFactory is often cleaner than carrying a scoped context around.

csharp
1using Microsoft.EntityFrameworkCore;
2
3public sealed class MetricsService
4{
5    private readonly IDbContextFactory<AppDbContext> _factory;
6
7    public MetricsService(IDbContextFactory<AppDbContext> factory)
8    {
9        _factory = factory;
10    }
11
12    public async Task<int> CountUsersAsync(CancellationToken ct)
13    {
14        await using var db = await _factory.CreateDbContextAsync(ct);
15        return await db.Users.CountAsync(ct);
16    }
17}

This pattern makes ownership explicit. The method creates the context, uses it, and disposes it in one place.

Lazy Execution Can Hide the Real Bug

Another failure mode is returning an IQueryable or deferred sequence from a scope that is already ending.

csharp
1public IQueryable<User> QueryActiveUsers()
2{
3    return _db.Users.Where(u => u.IsActive);
4}

The query is not executed yet. If enumeration happens after the context is gone, the error appears later and looks confusing. Materialize the data inside the valid lifetime if the caller should not own the context.

csharp
1public async Task<List<User>> GetActiveUsersAsync(CancellationToken ct)
2{
3    return await _db.Users.Where(u => u.IsActive).ToListAsync(ct);
4}

Common Pitfalls

  • Starting fire-and-forget tasks that capture scoped services such as DbContext.
  • Returning deferred queries that are executed only after the context has already been disposed.
  • Using Task.Run as if it were a fix for dependency lifetime problems.
  • Sharing one context across concurrent operations without a clear ownership boundary.
  • Forgetting await using when a factory-created context supports asynchronous disposal.

Summary

  • Disposed-context errors in async code are usually lifetime mismatches.
  • Await request-scoped work before the scope ends.
  • For background tasks, create a new dependency-injection scope or use a context factory.
  • Materialize results inside the valid context lifetime when callers should not own the query.
  • Treat disposal problems as ownership-design issues, not as random async glitches.

Course illustration
Course illustration

All Rights Reserved.