Recently, my boss and I were talking about Dependency Injection and some of the hidden problems you might face in large .NET applications. During the discussion, we came across the issue of captive dependencies—a mistake many developers make without noticing it. As we talked through what the problem is, why it happens, and how to fix it, I felt it was something worth explaining to others. That conversation encouraged me to write this blog.
In .NET, a Captive Dependency happens when a long-living service (like a Singleton) depends on a short-living service (Scoped or Transient). This causes the short-living service to “stick around” much longer than it should. Because of this, your app may use outdated data, show strange behavior between requests, run into concurrency issues, or even leak memory. The tricky part is that everything seems fine at first—until the problem suddenly appears in real scenarios.
Real-Time Example: Background Worker with DbContext
Imagine a .NET 8 application where a background worker processes messages from RabbitMQ:
public class MessageWorker : BackgroundService
{
private readonly AppDbContext _db; // Scoped
public MessageWorker(AppDbContext db) => _db = db;
protected override async Task ExecuteAsync(CancellationToken token)
{
var item = await _db.Orders.FirstAsync();
}
}
In this case, BackgroundService is a Singleton, but AppDbContext is Scoped. This combination creates a captive dependency. In a real environment, it can lead to problems such as:
“A second operation was started on this DbContext…”
stale or outdated data being reused
cross-thread issues when the same DbContext is used by multiple operations
The Correct Fix
The proper solution is to create a new scope for each iteration:
public class MessageWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public MessageWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken token)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var item = await db.Orders.FirstAsync();
}
}
Captive dependencies are easy to overlook but understanding them early prevents unpredictable runtime issues and ensures your .NET applications remain robust and reliable.
Happy Coding!