C#
Keep background jobs independent when a .NET worker runs continuously
Give each order import its own dependency scope, then check disposal and overlapping work before increasing throughput.
Published · AI disclosure
Imagine an order-import worker that stays alive for days. Each order should still have a clear start and finish, with the services it needs released afterward. An AI-generated change can compile while giving that long-running worker a processor whose dependencies were designed for much shorter work.
The review needs to answer three practical concerns:
- Can the worker start with dependency-scope validation enabled?
- Does each order get independent processing state?
- Can work finish before its dependencies are disposed, including during shutdown?
Start with one order. That makes the lifetime decision concrete enough to inspect and test.
Find what the worker keeps alive
AddHostedService<OrderWorker>() registers the worker as a singleton. A hosted service receives no automatic dependency scope. Injecting a scoped order processor directly into its constructor therefore creates a lifetime mismatch. Microsoft's scoped-worker guidance explains the required boundary.
Trace the processor's dependencies too. An apparently stateless processor might depend on an EF Core DbContext, which AddDbContext registers as scoped by default. That context tracks changes for a unit of work and is designed to be short-lived. Keeping it for unrelated orders extends that state beyond the intended job. EF Core lifetime guidance
With scope validation enabled, the container can reject scoped dependencies captured by a singleton. Resolving the same dependency through the root service provider does not repair the design: its lifetime effectively extends until application shutdown. Dependency-injection scope validation
Where should one order's scope begin and end?
Create it after receiving the order, resolve the processor inside it, and await processing before leaving it.
This example targets an existing .NET 10 Worker project. Register OrderWorker with AddHostedService and OrderProcessor with AddScoped. Keep the processor's configured AddDbContext registration scoped. Here, IOrderQueue is an existing singleton, cancellation-aware queue abstraction whose ReadAllAsync yields order IDs.
The C# example was checked against documentation; it has not been compiled or run in a .NET application.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public sealed class OrderWorker(
IServiceScopeFactory scopes,
IOrderQueue queue) : BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
await foreach (Guid orderId in queue.ReadAllAsync(stoppingToken))
{
await using var scope = scopes.CreateAsyncScope();
var processor = scope.ServiceProvider
.GetRequiredService<OrderProcessor>();
await processor.ProcessAsync(orderId, stoppingToken);
}
}
}The worker holds the scope factory and queue. Each iteration owns a processor and its scoped dependencies. await using allows asynchronous cleanup when that iteration leaves the scope, including after an exception. Container-created disposable services belong to the container; the processor should not dispose injected dependencies itself. Dependency-injection disposal guidance
Keep the await. Starting ProcessAsync and immediately leaving the scope would let processing continue after disposal. Creating one scope outside the loop would instead keep its scoped instances across orders.
Add concurrency only after ownership is clear
The shown loop processes one order at a time. If more throughput is needed, each concurrent operation needs its own scope, created inside that operation, and the worker must await all started work. Bound concurrency according to the database and downstream service capacity.
EF Core does not support overlapping operations on one context. Making the context a singleton, or sharing one scope across parallel orders, does not make that use safe. Even separate scopes cannot prevent two orders from updating the same database record; transaction and concurrency rules still need their own design. DbContext threading rules
A scope also does not provide queue acknowledgement, retries, or duplicate protection. If saving an order succeeds and acknowledgement fails, the delivery policy must determine what happens next. Agree that behavior separately from the lifetime repair.
An unhandled processing failure escapes this loop and, with the default hosting policy in .NET 6 and later, stops the host. Decide the job-failure policy before using the example in a continuously running service. BackgroundService exception handling
What evidence should the review produce?
Ask AI to trace the registration graph and identify where each scoped instance is created, used, and disposed. Then verify its explanation with checks for this worker:
- Build the service provider with scope validation enabled and resolve the hosted service.
- Process two orders and assert that their scoped processor instances differ.
- Hold processing open and confirm disposal happens only after completion; repeat for failure and cancellation.
- If parallel processing is introduced, check separate contexts and correct business results under the agreed concurrency.
These checks establish lifetime behavior for the tested paths. Database integration and delivery-recovery tests establish whether orders actually complete correctly. Keep both in the review evidence.
A straightforward scope repair can be handled internally. If a business-critical worker still has unclear behavior during overlapping jobs, shutdown, or failure, a focused independent review can produce a lifetime trace and tests for those uncertain paths, with retry and acknowledgement gaps recorded separately.