Asynchronous Requests
EntityFramework
Dependency Injection
Web Development
Programming Techniques

Asynchronous Web Request, EntityFramework, and DI, how does it work?

Master System Design with Codemia

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

Introduction

In ASP.NET applications, asynchronous web requests, Entity Framework, and dependency injection work together through request-scoped object lifetimes and async continuation flow. The main idea is simple: the request begins, the DI container creates scoped services such as DbContext, async I/O frees the thread while waiting, and the same request scope remains valid until the request ends. Most confusion comes from thinking async creates a new request scope or a new DbContext automatically, which it does not.

The Core Pieces

These three concepts play different roles:

  • asynchronous web requests avoid blocking a worker thread during I/O
  • Entity Framework provides database access through a DbContext
  • dependency injection creates and supplies objects such as repositories and contexts

They are related at runtime because an HTTP request commonly resolves a service graph that includes a scoped DbContext, then uses async methods such as ToListAsync() while the request is still in progress.

A Typical Request Flow

A simplified flow looks like this:

  1. an HTTP request reaches the controller or endpoint
  2. the DI container resolves the controller and its dependencies
  3. a scoped DbContext is created for that request
  4. async EF operations release the thread while waiting on the database
  5. the request resumes when the awaited work completes
  6. the scoped services are disposed when the request finishes

The important point is that async changes thread usage, not the meaning of the request scope.

Example with Controller, Service, and DbContext

Here is a small ASP.NET Core example.

csharp
1using Microsoft.AspNetCore.Mvc;
2using Microsoft.EntityFrameworkCore;
3
4public class User {
5    public int Id { get; set; }
6    public string Name { get; set; } = string.Empty;
7}
8
9public class AppDbContext : DbContext {
10    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
11    public DbSet<User> Users => Set<User>();
12}
13
14public class UserService {
15    private readonly AppDbContext _db;
16
17    public UserService(AppDbContext db) {
18        _db = db;
19    }
20
21    public Task<List<User>> GetUsersAsync() {
22        return _db.Users.OrderBy(u => u.Name).ToListAsync();
23    }
24}
25
26[ApiController]
27[Route("api/users")]
28public class UsersController : ControllerBase {
29    private readonly UserService _service;
30
31    public UsersController(UserService service) {
32        _service = service;
33    }
34
35    [HttpGet]
36    public async Task<ActionResult<List<User>>> Get() {
37        var users = await _service.GetUsersAsync();
38        return Ok(users);
39    }
40}

This is the common shape of an async web request using DI and EF.

Register the Services with the Right Lifetime

The standard lifetime for DbContext in web apps is scoped.

csharp
1using Microsoft.EntityFrameworkCore;
2
3var builder = WebApplication.CreateBuilder(args);
4
5builder.Services.AddDbContext<AppDbContext>(options =>
6    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
7
8builder.Services.AddScoped<UserService>();
9builder.Services.AddControllers();

Scoped means one instance per request by default. That fits EF well because the context tracks entities and should not be shared across unrelated requests.

What Async Actually Changes

When your code awaits a database or HTTP call, the request does not block a thread the whole time. The thread can return to the pool while the I/O is pending, then another thread can continue the request later.

What stays the same:

  • the logical request scope
  • the scoped services resolved for that request
  • the DbContext lifetime, unless you explicitly create a new one

So async is about efficient waiting, not about starting a fresh dependency graph.

Why DbContext Should Not Be Singleton

One of the easiest ways to break this model is to treat DbContext like a singleton. EF contexts are not thread-safe and are not meant to be shared across concurrent requests.

Correct mental model:

  • one request gets one scoped context
  • async calls on that request can use that context sequentially
  • different requests get different contexts

That is why the DI lifetime matters so much here.

Async All the Way Matters Too

If you call async APIs but then block on them with .Result or .Wait(), you lose most of the benefits and can create deadlocks or throughput problems.

Prefer:

csharp
var users = await _db.Users.ToListAsync();

Avoid in request code:

csharp
var users = _db.Users.ToListAsync().Result;

The right pattern is to stay async from controller to service to data access where possible.

Common Pitfalls

  • Assuming await creates a new DI scope or a new DbContext.
  • Registering DbContext with the wrong lifetime, especially singleton.
  • Mixing async EF methods with blocking .Result or .Wait() calls.
  • Sharing one DbContext across multiple concurrent operations manually.
  • Treating async as a performance trick while ignoring lifetime and threading rules.

Summary

  • DI creates scoped services such as DbContext for each web request.
  • Async web requests free threads during I/O, but the logical request scope stays intact.
  • Entity Framework works well with async methods such as ToListAsync() when the context is scoped.
  • 'DbContext should not be singleton or shared across unrelated concurrent work.'
  • The safest design is async all the way with correct DI lifetimes and request-scoped data access.

Course illustration
Course illustration

All Rights Reserved.