Autofac
Async Factory
Dependency Injection
C# Programming
Software Development

Autofac Registering an Async Factory method

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Autofac can register factories that return Task<T>, but Autofac resolution itself is fundamentally synchronous. That distinction is the key to this topic: you do not make the container "await" component construction during Resolve; instead, you register a factory delegate that your application code can call and await later.

The Important Limitation

Developers often try to do something like "register this service with an async lambda so Autofac builds it asynchronously." Autofac does not work that way. Container activation and resolution are synchronous operations.

So this is the wrong mental model:

csharp
// Conceptually tempting, but the container itself is not doing async resolution.
builder.Register(async ctx => await CreateThingAsync());

The better model is:

  • register normal synchronous dependencies as usual,
  • register an async factory delegate if object creation requires async work,
  • await that factory in application code.

Register a Factory Returning Task<T>

Here is a practical pattern:

csharp
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4using Autofac;
5
6public class ExpensiveClient
7{
8    private readonly HttpClient _http;
9
10    public ExpensiveClient(HttpClient http)
11    {
12        _http = http;
13    }
14
15    public async Task InitializeAsync()
16    {
17        await Task.Delay(100); // simulate I/O
18    }
19}
20
21var builder = new ContainerBuilder();
22
23builder.RegisterType<HttpClient>().SingleInstance();
24
25builder.Register(c =>
26{
27    var context = c.Resolve<IComponentContext>();
28
29    return new Func<Task<ExpensiveClient>>(async () =>
30    {
31        var client = new ExpensiveClient(context.Resolve<HttpClient>());
32        await client.InitializeAsync();
33        return client;
34    });
35});

Now resolve the factory, not the final object:

csharp
1using var container = builder.Build();
2
3var factory = container.Resolve<Func<Task<ExpensiveClient>>>();
4var client = await factory();

This keeps Autofac's responsibility synchronous while still allowing async initialization in your application flow.

Why This Pattern Works

The container is resolving a plain delegate synchronously. That delegate happens to return Task<ExpensiveClient>, which your code can await later.

That is different from trying to make Autofac itself suspend the resolution pipeline.

This distinction is subtle but important:

  • Autofac resolves the factory synchronously,
  • your code invokes and awaits the factory asynchronously.

Another Option: Separate Construction From Initialization

Often the cleanest design is to keep constructors cheap and synchronous, then expose an explicit async initialization step outside DI.

csharp
1public class CacheWarmupService
2{
3    public Task WarmUpAsync()
4    {
5        return Task.Delay(100);
6    }
7}

Register the type normally:

csharp
builder.RegisterType<CacheWarmupService>().SingleInstance();

Then, after resolution:

csharp
var service = container.Resolve<CacheWarmupService>();
await service.WarmUpAsync();

This is often easier to reason about than hiding async work inside a DI registration.

Avoid Blocking on Async in Registration

A common anti-pattern is forcing async code into a synchronous registration with .Result or .GetAwaiter().GetResult():

csharp
builder.Register(c => CreateThingAsync().GetAwaiter().GetResult());

That can block threads, complicate error handling, and create deadlock risks depending on the surrounding application model.

If the creation is truly asynchronous, prefer a factory returning Task<T> or redesign the startup flow so the async step happens outside container registration.

Lifetime Considerations

Be careful with lifetimes. If the async factory creates a scoped or disposable object, the scope that resolved its dependencies must still make sense by the time initialization completes.

In other words, async initialization does not remove normal lifetime responsibilities. If anything, it makes them more important because the object's creation now spans time.

When a Factory Is the Right Tool

An async factory is a good fit when:

  • initialization needs network or disk I/O,
  • startup should be deferred until the service is actually needed,
  • or multiple differently configured instances may be created on demand.

If the service is a singleton that must always be ready at app start, a dedicated startup phase is often clearer than hiding that work behind DI.

Common Pitfalls

The biggest pitfall is assuming Autofac can perform truly asynchronous Resolve operations. It cannot; resolution is synchronous.

Another mistake is blocking on async work inside the registration delegate. That often turns an asynchronous design into a brittle synchronous one.

Developers also sometimes hide too much initialization inside constructors. Constructors should stay cheap and predictable whenever possible.

Finally, watch object lifetime carefully when async factories create disposable or scoped services.

Summary

  • Autofac resolution is synchronous, even if your object needs async initialization.
  • The usual solution is to register a factory that returns Task<T>.
  • Your application code resolves the factory synchronously and awaits it later.
  • Avoid forcing async work into registration with .Result or .GetAwaiter().GetResult().
  • In many designs, explicit post-resolution initialization is simpler than async constructor-style logic.

Course illustration
Course illustration

All Rights Reserved.