Autofac: Disposer prevents objects from being garbage collected

Created on 30 Nov 2020  路  7Comments  路  Source: autofac/Autofac

Describe the Bug

AutoFac supports automatic disposal of objects when the ILifetimeScope is disposed. In order to do so, disposable objects are added to the lifetime scope's Disposer object, which keeps reference to these objects in a Stack<object>. When the lifetime scope is disposed, the Disposer pops each object from its stack and disposes it.

The problem is, this mechanism helps dispose the objects when the lifetime is disposed, but also _prevents_ them from being garbage collected before the lifetime scope is disposed. A client might resolve a disposable object and dispose it immediately when they're done with it. However, because Disposer still has a reference to the object, it cannot be garbage collected until the lifetime scope is disposed.

Ideally, the lifetime scope should auto-dispose items like it's doing today, but at the same time, it should not prevent objects from being garbage collected if nothing else is holding a reference to them.

Steps to Reproduce

```c#
public class ReproTest
{
private class MyDisposable : IDisposable
{
public Action OnFinalize;

    ~MyDisposable() => OnFinalize();

    public void Dispose() { }
}

private static void DoWork(ILifetimeScope container, Action onCompleted)
{
    var obj = container.Resolve<MyDisposable>();
    obj.OnFinalize = onCompleted;
}

[Fact]
public void Repro()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<MyDisposable>();
    var container = builder.Build();
    bool finalized = false;

    DoWork(container, () => finalized = true);
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Assert.False(finalized);  // object is still alive because the container holds a reference to it

    container.Dispose();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Assert.True(finalized);  // object is now finalized because disposing the container removed the ref
}

}
```

question wontfix

All 7 comments

It feels like there's a conflict here. If we don't keep a proper reference to the object that was created, how can we call Dispose on it when the lifetime scope is disposed?

It'll end up being finalised before we can dispose of it if we only hold a WeakReference, right?

@alistairjevans My proposed solution was that Disposer only disposes objects that haven't been finalized yet. In other words, the object would be responsible for making sure its finalizer calls its own Dispose().

The standard disposal pattern has a different path for disposing via a Finalizer as opposed to calling it manually. I'd generally considered to be an anti-pattern to rely on the Finalizer. It's supposed to be a last-chance for releasing unmanaged resources if Disposal isn't done properly.

I'd argue that if you need an object to be GC'ed as soon as nothing is using it within a scope, then it should be marked as ExternallyOwned, so Autofac doesn't track it or attempt disposal, and any Dispose activity would have be manually implemented via the Finalizer as you describe.

Ok, you're right. If a client releases object references without calling Dispose, that might prevent proper disposal of the object. (Maybe the client doesn't even know that the object is IDisposable. Its dependency type might not be IDisposable, but the actual injected object might be IDisposable.) So I agree that my proposed solution won't work well in all cases as is.

On the other hand, I have a use-case where we have a long running windows service app, and the type registrations are being done in a class library, within an extension method to IServiceCollection. The class lib doesn't reference AutoFac so, ExternallyOwned isn't available at the time of type registration.

Additionally, the service app isn't aware transactions that are taking place within the class lib, so it cannot control child scopes. We end up with a single/root scope that isn't disposed (until the app stops) and the class lib keeps resolving disposable components that are accumulated in the Disposer and never collected.

So with class libs, it might not be within our control to use child scopes or ExternallyOwned.

If you add a Module, you can get notified of every registration that gets added to the root container, and set RegistrationData.Ownership = InstanceOwnership.ExternallyOwned manually on those existing registrations not directly under your control, if you're able to identify the specific problematic registrations.

Was this page helpful?
0 / 5 - 0 ratings