Async Programming
Simple Injector
Lifetime Management
Dependency Injection
.NET

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.

csharp
1using SimpleInjector;
2using SimpleInjector.Lifestyles;
3
4var container = new Container();
5container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
6container.Register<IUnitOfWork, SqlUnitOfWork>(Lifestyle.Scoped);

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.

csharp
1public sealed class OrderService
2{
3    private readonly IUnitOfWork unitOfWork;
4
5    public OrderService(IUnitOfWork unitOfWork)
6    {
7        this.unitOfWork = unitOfWork;
8    }
9
10    public async Task SaveAsync(Order order)
11    {
12        await unitOfWork.SaveOrderAsync(order);
13        await unitOfWork.CommitAsync();
14    }
15}

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.

csharp
1public Task HandleAsync()
2{
3    _ = Task.Run(async () =>
4    {
5        await unitOfWork.CommitAsync();
6    });
7
8    return Task.CompletedTask;
9}

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.

csharp
1using SimpleInjector;
2using SimpleInjector.Lifestyles;
3
4public sealed class BackgroundJobRunner
5{
6    private readonly Container container;
7
8    public BackgroundJobRunner(Container container)
9    {
10        this.container = container;
11    }
12
13    public async Task RunAsync()
14    {
15        using (AsyncScopedLifestyle.BeginScope(container))
16        {
17            var worker = container.GetInstance<IReportGenerator>();
18            await worker.GenerateAsync();
19        }
20    }
21}

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.

csharp
1using (AsyncScopedLifestyle.BeginScope(container))
2{
3    var handler = container.GetInstance<IMessageHandler>();
4    await handler.HandleAsync(message);
5}

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 AsyncScopedLifestyle for asynchronous flows.
  • Injecting scoped dependencies into singletons.
  • Reusing a scoped object across multiple unrelated jobs.
  • Assuming await automatically makes lifetime problems disappear.

Summary

  • Scoped dependencies in Simple Injector are only valid inside their active scope.
  • 'AsyncScopedLifestyle allows 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.

Course illustration
Course illustration

All Rights Reserved.