DI035

Non-Thread-Safe Service Shared Across a Fan-Out

A documented non-thread-safe service — an EF Core DbContext or a derived context, IDbContextTransaction, or an ADO.NET connection, command, transaction, or reader — declared outside a Task.WhenAll projection and used inside every one of its tasks. This includes a service created once per outer SelectMany group and then shared by the inner tasks flattened into the same WhenAll.

Warning Default severity · Code fix: No

Why it matters

Task.WhenAll starts every task before awaiting any of them, so the projection's lambda runs concurrently on one shared instance. DbContext detects it and throws *"A second operation was started on this context before a previous operation completed"*; the ADO.NET types are less forgiving and can corrupt connection state instead. The code reads like a clean parallel speed-up, which is exactly why it survives review.

README problem example

await Task.WhenAll(orderIds.Select(id => _db.LoadAsync(id)));  // DI035

README better pattern

await Task.WhenAll(orderIds.Select(async id =>
{
    await using var scope = _scopeFactory.CreateAsyncScope();
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.LoadAsync(id);
}));

No — the repair is a choice between a scope per task and sequential processing, with different throughput consequences.

Guardrails

When DI035 stays silent

Only the selector of a Select/SelectMany, or a lambda handed to WhenAll directly, counts as a concurrent body — a Where predicate runs one element at a time during enumeration, and an unrelated lambda nested inside a selector is part of that selector's single task. The exception is an inner task-returning selector directly returned by an exact System.Linq.Enumerable.SelectMany collection selector: those tasks are flattened into the outer WhenAll, so a value declared in the outer selector is shared by the inner fan-out. Exact LINQ binding, exact Task return, and return ownership are required; returns inside nested lambdas or local functions do not qualify. Properties are excluded, since a computed property can return a fresh instance per access. Only values declared *outside* the concurrent body count — a context created or resolved inside the actual task lambda belongs to that one task. Thread-safe services are untouched, nameof is not a use, and a sequential foreach never fans out. Parallel.For/ForEach/ForEachAsync bodies and framework message handlers are DI021's territory; this rule covers the Task.WhenAll leg it documented as out of scope.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app shared-context fan-out warning

    public sealed class Bad_SharedContextFanOut
    {
        private readonly AppDbContext _db;

        public Bad_SharedContextFanOut(AppDbContext db)
        {
            _db = db;
        }

        public async Task ProcessAsync(IEnumerable<int> orderIds)
        {
            // DI035: every task runs against the same DbContext at the same time.
            await Task.WhenAll(orderIds.Select(id => _db.LoadAsync(id)));
        }
    }

Sample app scope-per-task pattern

    public sealed class Good_ScopePerTaskFanOut
    {
        private readonly IServiceScopeFactory _scopeFactory;

        public Good_ScopePerTaskFanOut(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }

        public async Task ProcessAsync(IEnumerable<int> orderIds)
        {
            await Task.WhenAll(
                orderIds.Select(async id =>
                {
                    await using var scope = _scopeFactory.CreateAsyncScope();
                    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
                    await db.LoadAsync(id);
                }));
        }
    }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules