.NET DI: The Scoped vs Transient Bug That Corrupts Data

.NET DI: Scoped बनाम Transient बग जो डेटा खराब करता है

The bug that works in dev

You inject AppDbContext into a background service. It works locally. It passes code review. It runs in staging for a week. Then production hits 50 concurrent requests and your data starts mysteriously corrupting — entities saved on top of each other, updates lost, the occasional ObjectDisposedException.

You just hit the captive dependency problem.

What happened

AppDbContext is registered as Scoped (default when you call AddDbContext). A Scoped service lives for the duration of one HTTP request.

Your BackgroundService is a Singleton — one instance for the app's lifetime.

When you inject Scoped into Singleton, the Singleton captures the very first Scoped instance and holds it forever. That single DbContext is now shared across every background-service iteration. DbContext is not thread-safe. Two concurrent tasks mutating the same change-tracker = corruption.

The rule of thumb

  • Transient → Scoped → Singleton: fine.
  • Scoped → Singleton: dangerous.
  • Singleton → Scoped: impossible (DI throws if you try).

A Singleton must never hold a direct reference to a Scoped service.

The fix: inject the factory, not the service

public class NightlyReportService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public NightlyReportService(IServiceScopeFactory scopeFactory)
        => _scopeFactory = scopeFactory;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            // do work with a fresh DbContext...

            await Task.Delay(TimeSpan.FromHours(1), ct);
        }
    }
}

Each iteration gets its own scope and its own DbContext. Disposed when the scope is disposed. No shared state.

EF Core specifically: use the DbContext factory

builder.Services.AddDbContextFactory<AppDbContext>(...);

// then in your service:
using var db = await _factory.CreateDbContextAsync();

AddDbContextFactory is explicitly designed for this. Use it in Blazor Server, background services, minimal APIs that spawn tasks — anywhere the request scope isn't right.

Catch it with validation

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

Now the DI container refuses to resolve Singletons that capture Scopes. Fails at startup instead of silently eating your data three weeks in.

Takeaway

Captive dependencies are silent in dev and catastrophic in production. Never inject Scoped into Singleton. Use IServiceScopeFactory or IDbContextFactory. Enable ValidateScopes.

हिंदी में

Scoped service को Singleton में inject करना खतरनाक है। Singleton पहले Scoped instance को हमेशा के लिए पकड़ लेता है। DbContext thread-safe नहीं है, इसलिए concurrent requests में data corruption होता है।

समाधान: IServiceScopeFactory या IDbContextFactory inject करें। हर iteration में नया scope बनाएं:

using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

बिल्ड-टाइम जांच:

opts.ValidateScopes = true;
opts.ValidateOnBuild = true;

इससे स्टार्टअप पर ही गलती पकड़ी जाती है, production में data खराब होने से पहले।