DI024

Hosted Service Creates Scope Outside Execution Loop

A `BackgroundService.ExecuteAsync` override (or `IHostedService`/`IHostedLifecycleService` start method) that creates an `IServiceScope` once **before** its long-running execution loop — including direct or compound cancellation checks, `while (true)`, `for (;;)`, `PeriodicTimer` loops, and channel-consumer loops — and uses it inside the loop, either directly or through a service resolved from it. One-hop, directly invoked private helpers in the same type declaration receive the same helper-local analysis; deferred and transitive helper calls stay conservative. Generic and direct-`typeof(T)` non-generic `GetService`/`GetRequiredService` resolutions participate, including keyed `GetKeyedService`/`GetRequiredKeyedService` calls with compile-time keys; runtime `Type` values and dynamic keys stay conservative. Compound conditions stay conservative: nested `!` operators are reduced by polarity, every `&&` operand must be long-running, while one long-running `||` operand is sufficient; negated cancellation combinations use De Morgan semantics. It also catches a service whose registration is provably scoped resolved once before the loop and reused across iterations.

Default severity: Warning · Code fix: No

Why it matters

The well-known hosted-service idiom is *scope per iteration*. A scope hoisted above the loop keeps the same scoped instances alive for the entire process lifetime: an EF Core `DbContext` serves stale data and its change tracker grows without bound, a unit of work accumulates every iteration's state, and a single failure poisons all subsequent iterations.

README problem example

README better pattern

No. Moving the scope into the loop is a statement-level rewrite with disposal implications; apply the correct pattern above manually.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app hoisted-scope warning

    public class Bad_HoistedScopePollingService : BackgroundService
    {
        private readonly IServiceScopeFactory _scopeFactory;

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

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // [DI024] Scope is created outside the execution loop of 'ExecuteAsync'
            await using var scope = _scopeFactory.CreateAsyncScope();
            while (!stoppingToken.IsCancellationRequested)
            {
                var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
                await processor.ProcessPendingAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
    }

Sample app scope-per-iteration pattern

    public class Good_ScopePerIterationPollingService : BackgroundService
    {
        private readonly IServiceScopeFactory _scopeFactory;

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

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                // Each iteration gets fresh scoped services.
                await using var scope = _scopeFactory.CreateAsyncScope();
                var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
                await processor.ProcessPendingAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }
    }

Related guides

  • No problem-guide pages point here yet.