I came across a problem on a recent WebAPI project where I wanted to use Autofac to inject some tenant information (i.e. derived per request) into the constructor of each controller:
public class MyController : ApiController
{
public MyController(TenantInformation tenantInfo)
{
}
}
The problem was that the TenantInformation
had to be sourced from an async API call
var tenantInfo = await tenantApi.GetTenantInfoAsync();
This means that you cannot implement something like the below to register the component
static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.Register(context => context.Resolve<TenantApi>().GetTenantInfo());
var container = builder.Build();
var example = container.Resolve<ExampleController>();
// --> throws 'Autofac.Core.Registration.ComponentNotRegisteredException'
}
On closer examination of container
we can see that TenantInfo
has not been registered; instead we have registered an instance of Task<TenantInfo>
. We can await this but not from a constructor.
One option that I briefly considered was importing the service directly into each controller and then getting the value within each async action method that required it. This works but it feels messy and against the point of DI. I want to be able to depend on my dependencies; not on the providers of my dependencies.
Using a Mediator
My solution was to create a mediator object representing an asynchronously-resolved component:
interface IAsyncRegistration
{
Task Resolve(IComponentContext context);
}
class AsyncRegistration<T> : IAsyncRegistration
{
private Func<IComponentContext, Task<T>> _resolve;
public AsyncRegistration(Func<IComponentContext, Task<T>> resolve)
{
_resolve = resolve;
}
public bool Resolved { get; private set; }
public T Value { get; private set; }
public async Task Resolve(IComponentContext context)
{
this.Value = await _resolve(context);
this.Resolved = true;
}
}
This class wraps a resolution function for the type, the resolved value and a flag to indicate whether or not it has been resolved. It also implements a non-generic interface so we can find all instances of AsyncRegistration<T>
regardless of T
.
public static IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle> RegisterAsync<T>(this ContainerBuilder builder, Func<IComponentContext, Task<T>> resolve)
{
builder.RegisterInstance(new AsyncRegistration<T>(resolve))
.AsSelf()
.AsImplementedInterfaces();
return builder.Register<T>(context =>
{
var asyncRegistration = context.Resolve<AsyncRegistration<T>>();
if (!asyncRegistration.Resolved)
throw new DependencyResolutionException($"Async component {typeof(T).Name} has not been resolved");
return asyncRegistration.Value;
});
}
Next I created an extension method for ContainerBuilder
that adds 2 registrations:
- A registration of
AsyncRegistration<T>
- A registration of
<T>
that resolves theAsyncRegistration<T>
, checks that it has been resolved and then returns the result
Finally I created an extension method that can be called on the container from anywhere within an async block that will resolve all of the values
public static Task ResolveAsyncRegistrations(this IComponentContext context)
{
var registrations = context.Resolve<IEnumerable<IAsyncRegistration>>();
return Task.WhenAll(registrations.Select(r => r.Resolve(context)));
}
All together this means that the following will work and we can now inject asynchronously-resolved services into controller constructors:
var builder = new ContainerBuilder();
builder.RegisterAsync(context => context.Resolve<TenantApi>().GetTenantInfo());
var container = builder.Build();
//...in an async block...
await container.ResolveAsyncRegistrations();
//...then some time later...
var tenantInfo = container.Resolve<TenantInfo>();
Plugging in to WebAPI
The easiest way to plug this in to the WebAPI pipeline is to create a message handler that
- Gets an
IComponentContext
for the current request - awaits a call to the
ResolveAsyncRegistrations
extension method
public class AsyncRegistrationHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var scope = request.GetDependencyScope().GetRequestLifetimeScope();
await scope.RegisterAsyncComponents();
return await base.SendAsync(request, cancellationToken);
}
}
Caveats
This system works for my particular scenario but there are a lot of possible situations where this would not work or would need extending. The lifetime management of the dependencies, for example, is very rigid in this implementation and would need some work to be exposed properly.