-
-
Notifications
You must be signed in to change notification settings - Fork 843
Description
Description
If you use InstancePerMatchingLifetimeScope("name") inside a child scope with a shorter lifetime than the named scope, then a new object is created per child scope, but not released until the outer named scope is disposed.
If multiple child scopes are created within a single outer named scope, this presents as a memory leak as objects which should logically be released at the child scope level are retained for longer than they should be.
Steps to Reproduce
This program demonstrates the problem, if you run it as a console app and observe the output:
using Autofac;
public static class Program
{
public static void Main()
{
var builder = new ContainerBuilder();
using var container = builder.Build();
Console.WriteLine("begin named scope");
var namedScope = container.BeginLifetimeScope("named");
for (int i = 0; i < 5; i++)
{
Console.WriteLine($" begin inner scope {i}");
var innerScope = namedScope.BeginLifetimeScope(b =>
{
b.RegisterType<C1>().InstancePerMatchingLifetimeScope("named");
});
innerScope.Resolve<C1>();
innerScope.Dispose();
Console.WriteLine($" ended inner scope {i}");
}
namedScope.Dispose();
Console.WriteLine("ended named scope");
}
class C1 : IDisposable
{
private readonly string uniqueId = Guid.NewGuid().ToString("N")[..7];
public C1() => Console.WriteLine($" C1 #{uniqueId} created");
public void Dispose() => Console.WriteLine($" C1 #{uniqueId} disposed");
}
}I observe this:
begin named scope
begin inner scope 0
C1 #9c0ac13 created
ended inner scope 0
begin inner scope 1
C1 #bb70274 created
ended inner scope 1
begin inner scope 2
C1 #26f7fa7 created
ended inner scope 2
begin inner scope 3
C1 #91d93d0 created
ended inner scope 3
begin inner scope 4
C1 #5b4abb6 created
ended inner scope 4
C1 #5b4abb6 disposed
C1 #91d93d0 disposed
C1 #26f7fa7 disposed
C1 #bb70274 disposed
C1 #9c0ac13 disposed
ended named scope
Note how we get a new C1 instance with each inner scope, but none are disposed until the outer "named" scope is disposed at the end.
Expected Behavior
It's not entirely clear what the correct behaviour should be.
As a starting point, I would suggest that either:
- It should act the same as
SingleInstance() - It should throw an exception.
Swapping the inner scope creation to use SingleInstance() results in the following correct behaviour:
var innerScope = namedScope.BeginLifetimeScope(b =>
{
b.RegisterType<C1>().SingleInstance();
});output, which matches expectations:
begin named scope
begin inner scope 0
C1 #956e2c0 created
C1 #956e2c0 disposed
ended inner scope 0
begin inner scope 1
C1 #47b6bb4 created
C1 #47b6bb4 disposed
ended inner scope 1
begin inner scope 2
C1 #83b564a created
C1 #83b564a disposed
ended inner scope 2
begin inner scope 3
C1 #57d97ef created
C1 #57d97ef disposed
ended inner scope 3
begin inner scope 4
C1 #1d190ce created
C1 #1d190ce disposed
ended inner scope 4
ended named scope
For clarity: I don't think using InstancePerMatchingLifetimeScope in a child lifetime scope setup action makes a lot of sense (at least not as described by the repro program).
The behaviour resulted in a real memory leak in our real application, but as the author of said code, what I wanted was the SingleInstance behaviour above, and I will update our code accordingly. From that perspective if Autofac threw an exception in this scenario it would be fine.
However, other autofac users could theoretically have scenarios where it might make sense with deeper levels of scoping, e.g. if the named lifetime scope is within the child lifetime scope rather than outliving it.
Dependency Versions
Autofac: 8.3.0
Also observed on 8.2.1
Additional Info
This may or may not be helpful, but as far as I can tell, the problem stems from the fact that LifetimeScope uses a GUID as the key for the _sharedInstances dictionary, and the GUID is created per-registration
What appeared to be happening when I ran this under the debugger was:
- Creating N child lifetime scopes results in N registrations, each with their own GUID
- Because the registration is configured as
InstancePerMatchingLifetimeScope("named"), autofac appears to run up the scope tree until it finds the outer named scope, then the object is created in, and stored by the outer scope.
This feels like a bug because logically a registration created at one level of scoping shouldn't be able to affect things at higher scopes, which it does here.