Skip to content

Commit 6caf240

Browse files
committed
Dispose container when startable or build callback throws (#1392)
If StartStartableComponents or RunBuildCallbacks threw during Build(), the newly created Container was abandoned without Dispose() being called. Any IDisposable instances that had been resolved into the root lifetime scope to satisfy those operations were then leaked. Wrap both calls in a try/catch that disposes the container and rethrows on the failure path; on the success path the caller owns the container as before. Also add a regression test that registers a DisposableDependency as a transitive dependency of a throwing IStartable, asserts Build() throws, and asserts the dependency was disposed.
1 parent f8d4095 commit 6caf240

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

src/Autofac/ContainerBuilder.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,21 @@ public IContainer Build(ContainerBuildOptions options = ContainerBuildOptions.No
175175
var componentRegistry = ComponentRegistryBuilder.Build();
176176

177177
var result = new Container(componentRegistry);
178-
if ((options & ContainerBuildOptions.IgnoreStartableComponents) == ContainerBuildOptions.None)
178+
try
179179
{
180-
StartableManager.StartStartableComponents(Properties, result);
181-
}
180+
if ((options & ContainerBuildOptions.IgnoreStartableComponents) == ContainerBuildOptions.None)
181+
{
182+
StartableManager.StartStartableComponents(Properties, result);
183+
}
182184

183-
// Run any build callbacks.
184-
BuildCallbackManager.RunBuildCallbacks(result);
185+
// Run any build callbacks.
186+
BuildCallbackManager.RunBuildCallbacks(result);
187+
}
188+
catch
189+
{
190+
result.Dispose();
191+
throw;
192+
}
185193

186194
// Allow the reflection cache to empty any registration-time caches to save memory.
187195
ReflectionCacheSet.Shared.OnContainerBuildClearCaches(_clearRegistrationCaches);

test/Autofac.Specification.Test/Features/StartableTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,23 @@ public void AutoActivate_RegisterInstanceActivationWorksWhenDefaultServiceOverlo
189189
Assert.Equal(1, instanceCount);
190190
}
191191

192+
[Fact]
193+
public void Startable_WhenStartableThrows_DependenciesAreDisposed()
194+
{
195+
// Issue #1392 - disposable instances resolved to satisfy a startable should be
196+
// disposed when the startable's Start() method throws.
197+
#pragma warning disable CA2000 // Dispose objects before losing scope - ownership transferred to container
198+
var dependency = new DisposableDependency();
199+
#pragma warning restore CA2000 // Dispose objects before losing scope
200+
var builder = new ContainerBuilder();
201+
builder.RegisterInstance(dependency);
202+
builder.RegisterType<ThrowingStartable>().As<IStartable>().SingleInstance();
203+
204+
Assert.Throws<DependencyResolutionException>(() => builder.Build());
205+
206+
Assert.True(dependency.IsDisposed);
207+
}
208+
192209
private class ComponentTakesStartableDependency : IStartable
193210
{
194211
public ComponentTakesStartableDependency(StartableTakesDependency dependency, bool expectStarted)
@@ -285,4 +302,29 @@ public void Start()
285302
WasStarted = true;
286303
}
287304
}
305+
306+
private class DisposableDependency : IDisposable
307+
{
308+
public bool IsDisposed
309+
{
310+
get; private set;
311+
}
312+
313+
public void Dispose()
314+
{
315+
IsDisposed = true;
316+
}
317+
}
318+
319+
private class ThrowingStartable : IStartable
320+
{
321+
public ThrowingStartable(DisposableDependency dependency)
322+
{
323+
}
324+
325+
public void Start()
326+
{
327+
throw new InvalidOperationException("Startable failed.");
328+
}
329+
}
288330
}

0 commit comments

Comments
 (0)