Async tasks and Simple Injector Lifetime scopes
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Asynchronous code does not remove lifetime-scope rules. If a dependency is registered with a scoped lifetime in Simple Injector, it still has to be used only while that scope is alive. The main mistake is starting background work that outlives the scope and then letting that work keep using scoped services.
What a Scope Means in Simple Injector
A scope is the lifetime boundary for services registered as scoped. Within the same scope, Simple Injector returns the same instance. Outside that scope, the object should not be reused.
In a web request, the request often defines the scope. In a manually constructed unit of work, you define the scope yourself.
AsyncScopedLifestyle is important because it allows the current scope to flow correctly through asynchronous continuations.
await Usually Preserves the Scope
A normal await inside the same logical operation is not the problem. If the asynchronous work is still part of the active scope, a scoped dependency can be resolved and used safely.
Here the service stays inside the same logical request or operation. That is fine as long as the surrounding scope remains open.
The Dangerous Pattern Is Fire-and-Forget Work
Problems appear when code starts a task and does not wait for it, especially if the task uses a scoped dependency after the request or operation has ended.
This is unsafe if unitOfWork is scoped. By the time the background task runs, the scope may already be disposed.
That can lead to disposed-object errors, inconsistent data access, or subtle bugs that only appear under load.
Create a New Scope for Background Work
If background work is truly needed, create a new scope inside that background operation and resolve fresh scoped services there.
This is the safe pattern because the background job owns its own lifetime boundary.
Do Not Cache Scoped Dependencies in Singletons
Another common mistake is injecting a scoped service into a singleton and storing it for later asynchronous use. That breaks lifetime rules even before async enters the picture.
A singleton may live for the whole application, while a scoped dependency is valid only within one scope. Simple Injector is deliberately strict about this because the mismatch is a real bug, not a style preference.
Scope Management in Non-Web Apps
In console apps, background workers, and message consumers, you often create scopes manually.
This pattern keeps scoped components tied to one message, one command, or one job. It is a good default in non-web code.
Async Does Not Mean Parallel Safety
It is also important not to confuse asynchronous flow with thread-safe shared use. A scoped object such as a database context may still be unsuitable for concurrent access from multiple tasks running at the same time.
The rule is simple: if one scope owns one unit of work, keep that unit of work coherent. Do not split it into uncontrolled parallel operations unless the dependencies were designed for that model.
Common Pitfalls
- Starting fire-and-forget tasks that keep using scoped services after the scope ends.
- Forgetting to configure
AsyncScopedLifestylefor asynchronous flows. - Injecting scoped dependencies into singletons.
- Reusing a scoped object across multiple unrelated jobs.
- Assuming
awaitautomatically makes lifetime problems disappear.
Summary
- Scoped dependencies in Simple Injector are only valid inside their active scope.
- '
AsyncScopedLifestyleallows scopes to flow correctly through awaited async code.' - Normal awaited work is usually safe if the scope stays open.
- Fire-and-forget background work must create its own scope.
- Do not cache or prolong scoped dependencies beyond the unit of work that owns them.

