DEV Community

Abhinaw
Abhinaw

Posted on • Originally published at bytecrafted.dev

5 ASP.NET Core DI Scope Mistakes That Will Crash Your App

Most of us treat dependency injection scopes like a checkbox: pick Singleton, Scoped, or Transient and move on. That confidence lasts until a 3 AM production crash, and you're staring at an ObjectDisposedException.

I learned this the hard way. Let's walk through the five most common DI scope mistakes I see in code reviews so you can avoid them.

1. Registering DbContext as Singleton

This is the fastest way to corrupt data. DbContext is not thread-safe and holds tracked entities. As a singleton, it creates massive memory leaks and concurrency bugs.

The Mistake

// DON'T DO THIS
services.AddSingleton<AppDbContext>();
Enter fullscreen mode Exit fullscreen mode

The Fix
Always register DbContext as scoped. This is the default behavior and ensures one instance per request.

// CORRECT
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
Enter fullscreen mode Exit fullscreen mode

2. Injecting Scoped Services into Singletons

A singleton lives forever; a scoped service dies with the request. Trying to inject a scoped service (like a repository using DbContext) into a singleton will cause an InvalidOperationException.

The Mistake

public class MySingletonService
{
    // You can't inject a scoped service here!
    public MySingletonService(IUserRepository userRepo) { ... }
}

services.AddSingleton<MySingletonService>();
services.AddScoped<IUserRepository, UserRepository>();
Enter fullscreen mode Exit fullscreen mode

The Fix
If you must access a scoped service from a singleton, inject IServiceScopeFactory and create a scope manually.

public class MySingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public async Task DoWorkAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
        // ... use the repo
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Forgetting Scopes in Background Services

BackgroundService instances are singletons and run outside the request pipeline, so there's no automatic scope. Injecting DbContext directly will cause it to live forever, leading to memory leaks.

The Mistake

public class NotificationService : BackgroundService
{
    private readonly AppDbContext _dbContext; // WRONG! Lives forever.

    public NotificationService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        // This uses the same DbContext instance for days...
        var items = await _dbContext.Notifications.ToListAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

The Fix
Use IServiceScopeFactory to create a new scope for each unit of work inside your ExecuteAsync loop.

public class NotificationService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public NotificationService(IServiceScopeFactory scopeFactory) { ... }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                // DbContext is disposed correctly after each loop
                var items = await dbContext.Notifications.ToListAsync(token);
            }
            await Task.Delay(TimeSpan.FromMinutes(5), token);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Misunderstanding Middleware Scopes

Middleware classes are singletons—created once for the application's lifetime. If you inject a scoped service into the constructor, you capture the service from the first request and reuse it for all subsequent requests, leading to data bleeding between users.

The Mistake

public class TenantMiddleware
{
    private readonly ITenantService _tenantService; // Scoped, but captured!

    // Constructor injection captures the service
    public TenantMiddleware(RequestDelegate next, ITenantService tenantService)
    {
        _tenantService = tenantService;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Using the same tenant service instance for every request!
        await _tenantService.DoWorkAsync();
        await _next(context);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Fix
Inject scoped services directly into the InvokeAsync method. The DI container resolves these dependencies per-request.

public class TenantMiddleware
{
    public TenantMiddleware(RequestDelegate next) { ... }

    // Scoped services are injected here, per-request
    public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
    {
        await tenantService.DoWorkAsync();
        await _next(context);
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Overusing Transient for Heavy Objects

Transient creates a new instance every single time an object is requested. This is fine for lightweight, stateless services, but it's a performance killer for heavy objects like HttpClient (can cause socket exhaustion) or compiled regex patterns.

The Mistake

// BAD: Creates a new client every time, can lead to socket exhaustion
services.AddTransient<HttpClient>();
Enter fullscreen mode Exit fullscreen mode

The Fix
Use AddHttpClient for HttpClient management and register other heavy, stateless services as Singleton.

// GOOD: Manages HttpClient lifecycle and connection pooling
services.AddHttpClient<MyApiService>();

// GOOD: Compile once, reuse everywhere
services.AddSingleton<IRegexValidator>(sp => 
    new RegexValidator(MyCompiledRegex));
Enter fullscreen mode Exit fullscreen mode

Quick Checklist

  • Singleton: For stateless, thread-safe services (e.g., loggers, configuration).
  • Scoped: For services that need to maintain state within a single request (e.g., DbContext, Unit of Work).
  • Transient: For lightweight, stateless services that are cheap to create.

Take 5 minutes to audit your Program.cs file. A quick check now can save you a major production headache later.

Happy coding!

Read more

Top comments (0)