How to fix “resolve operation has already ended” exceptions in lambda Autofac registrations
At work today, we ran into an ObjectDisposedException
when writing a lambda Autofac registration:
This resolve operation has already ended. When registering components using lambdas, the IComponentContext ‘c’ parameter to the lambda cannot be stored. Instead, either resolve IComponentContext again from ‘c’, or resolve a Func<> based factory to create subsequent components from.
Solution
This error happens at runtime when attempting to resolve a component from a “captured” IComponentContext
that belonged to the Register
method:
var cb = new ContainerBuilder();
// maybe the IComponentContext was captured inside the body of a Lazy<>
cb.Register(cc =>
new ApplePie(new Lazy<Apple>(() => cc.Resolve<Apple>())));
// ...or captured as a parameter (and stored inside) the instance
cb.Register(cc => new ApplePie(cc));
// ...or captured inside the body of a Func<>
cb.Register<Func<Apple>>(cc => () => cc.Resolve<Apple>());
var container = cb.Build();
container.Resolve<ApplePie>().UseApple(); // ERROR! This resolve operation h...
The resolution is to resolve IComponentContext
outside of the capture context and resolve from it instead:
cb.Register(cc =>
{
var context = cc.Resolve<IComponentContext>();
return new ApplePie(new Lazy<Apple>(() => context.Resolve<Apple>()));
});
// depending on which pattern from above maybe instead
// ... new ApplePie(context)
// ... or () => context.Resolve<Apple>
As explained below, there are no meaningful downsides to this technique for 99.9% of projects. The impact is a very, very small performance hit as the IComponentContext
from cc.Resolve<IComponentContext>()
is a tiny bit slower than the parameter IComponentContext
(cc
).
Why does this work?
Disclaimer: This section discusses the internals of Autofac. All details are liable to change at any time. This section is accurate as of Autofac version 6.1.0.
Note: This section is deep in the weeds of Autofac internals. For a conceptual explainer of Autofac try An illustrated guide to Autofac.
The first thing to understand is that the IComponentContext
resolved inside Register
is not the same instance as the IComponentContext
which is a parameter.
cb.Register(cc => {
var context = cc.Resolve<IComponentContext>();
context != cc; // true
...
});
The parameter IComponentContext
(cc
) is a DefaultResolveRequestContext
, and the resolved IComponentContext
(context
) is a LifetimeScope
. Both of these classes implement IComponentContext
, an interface which exposes methods for resolving services from the container, but differ greatly in their implementation.
As its name implies, LifetimeScope
is designed to persist for the length of a lifetime scope, e.g. an HTTP request, resolving hundreds of thousands of services across multiple threads. LifetimeScope
is the smallest unit of “sharing” in Autofac, so this class must keep track (in a thread safe manner) any services that were created with an InstancePerLifetimeScope
registration.
By contrast to meticulous, long-lived LifetimeScope
, DefaultResolveRequestContext
is an IComponentContext
spun up exclusively for the lifetime of a single Resolve
call, to be disposed of immediately afterward. To keep this short-lived service inexpensive, DefaultResolveRequestContext
is not-thread safe.
Here’s an example of what this “lack of thread-safety” looks like:
public class Apple { }
// ...
var cb = new ContainerBuilder();
cb.RegisterType<Apple>();
cb.Register(cc =>
{
// attempt use 'cc' simulatenously across multiple threads
Enumerable.Range(0, 100)
.Select(_ => Task.Run(() => cc.Resolve<Apple>()))
.ToList();
// give the other threads space to execute before 'cc'
// is disposed
Thread.Sleep(500);
return new object();
});
// This error will appear 0-99 times (it is a race condition after all!):
// DependencyResolutionException: Circular dependency detected:
// System.Object -> Apple -> Apple.
cb.Build().Resolve<object>();
At first blush, the error appears nonsensical, Apple
doesn’t depend on Apple
, where is the “circular dependency?”
Because DefaultResolveRequestContext
is not designed to be used by multiple threads, it interprets the parallel Resolve<Apple>()
to be happening sequentially. Specifically, the CircularDependencyDetectorMiddleware
maintains a non-thread safe stack of Resolve
requests that occur in the current ResolveOperation
in order to detect circular dependencies. For example, if the constructor for Apple
took Apple
as a parameter, the middleware would find a circular dependency by recognizing that Apple
appears twice in the stack of requests. In our example, the CircularDependencyDetectorMiddleware
errors out because it believes that our parallel requests are a recursive resolution with a circular dependency!
Summary
-
Knowing that
DefaultResolveRequestContext
is short-lived and disposed at the conclusion of a resolution operation, helps us understand the error message a the top of this blog post. We cannot captureDefaultResolveRequestContext
for later use because it will have been disposed by then! -
Knowing that
DefaultResolveRequestContext
is not thread safe in order to be cheap, helps us understand the tradeoffs of the “fix”.DefaultResolveRequestContext
does less bookkeeping thanLifetimeScope
. SpecificallyLifetimeScope
initializes a new stack (for circular dependency detection) for everyResolve
request, butDefaultResolveRequestContext
reuses the same stack each time.
It is important to note that DefaultResolveRequestContext
uses LifetimeScope
under the hood to manage creating and sharing instances. This means that both have identical business logic behavior. The only trade-off is that LifetimeScope
is a very small amount slower because of additional bookkeeping to ensure thread safety. Autofac is rarely a performance bottleneck, so 99.9% of projects can make this tradeoff without losing any sleep.
Curious about the philosophy toward thread safety in Autofac? The “Concurrency” section of their documentation is quite good.