Skip to content

Memory leak when registering InstancePerMatchingLifetimeScope() inside a BeginLifetimeScope lambda #1460

@borland

Description

@borland

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions