Closed Alison-97 closed 6 months ago
Have you tried this in the latest Autofac, 7.1.0? There have been some changes in internal caching behaviors as well as a fix around disposal (sync vs. async).
I put together a repro in Xunit based on your code but which will compile (e.g., there's no reference to PrinterService
in your objects so that got left out; I put in interface/implementation details):
public class UnitTest1
{
private readonly ITestOutputHelper _output;
public UnitTest1(ITestOutputHelper output)
{
this._output = output;
}
public interface IFooService
{
string Id { get; }
}
public class FooService : IFooService, IDisposable
{
private readonly ITestOutputHelper _output;
public FooService(ITestOutputHelper output)
{
this._output = output;
this.Id = Guid.NewGuid().ToString();
}
public string Id
{
get;
}
public void Dispose()
{
this._output.WriteLine($"Disposing FooService {this.Id}");
}
}
[Fact]
public void ResolveInUnitTest()
{
var container = GetContainerOnly();
var fooService = container.Resolve<IFooService>();
this._output.WriteLine($"Resolved FooService {fooService.Id}");
container.Dispose();
this._output.WriteLine("ResolveInUnitTest complete.");
}
[Fact]
public void ResolveFromMethod()
{
var (fooService, container) = GetContainerAndResolve();
this._output.WriteLine($"Resolved FooService {fooService.Id}");
container.Dispose();
this._output.WriteLine("ResolveFromMethod complete.");
}
private (IFooService, IContainer) GetContainerAndResolve()
{
var builder = new ContainerBuilder();
builder.RegisterType<FooService>().As<IFooService>();
builder.RegisterInstance(this._output);
var container = builder.Build();
var fooService = container.Resolve<IFooService>();
return (fooService, container);
}
private IContainer GetContainerOnly()
{
var builder = new ContainerBuilder();
builder.RegisterType<FooService>().As<IFooService>();
builder.RegisterInstance(this._output);
var container = builder.Build();
return container;
}
}
To make it easy to visualize, I basically did a Console.WriteLine during the object disposal so we can see what happens, whether Dispose
on the object is getting called or not. Here's the output:
Passed TestRepro.UnitTest1.ResolveInUnitTest [23 ms]
Standard Output Messages:
Resolved FooService c0960d1f-16fd-4ba3-8d4a-be487a3ee537
Disposing FooService c0960d1f-16fd-4ba3-8d4a-be487a3ee537
ResolveInUnitTest complete.
Passed TestRepro.UnitTest1.ResolveFromMethod [< 1 ms]
Standard Output Messages:
Resolved FooService cd4b16e9-bbd4-42be-a856-ba389d4b0f14
Disposing FooService cd4b16e9-bbd4-42be-a856-ba389d4b0f14
ResolveFromMethod complete.
As you can see, disposing the container does dispose of the stuff inside; at lest, Dispose
is called.
Looking at the documentation for dotMemory it appears that "Dead objects" isn't "what was disposed" but is:
The number of instances that existed in the base snapshot but were collected by the moment of taking a second snapshot.
From a profiler/benchmarking perspective, I know that we have always had to run a GC.Collect()
before getting a memory snapshot. We use BenchmarkDotNet for our benchmarks and that automatically does a collection before determining the difference in snapshots. I don't use dotMemory, but perhaps that's required for that tool as well?
I'll wait for feedback on this, but from what I can tell we're functioning as designed. To proceed here...
If I don't see this stuff in a week or so, I'll close the issue and we can reopen as needed later.
I'm using Autofac 7.1.0 and the problem persists.
This is the full code, including a concise implementation of the interface. I've updated the code to utilize dotMemory Unit, making it more apparent when the container disposal fails, resulting in an improper disposal of the fooService
object, thus causing the test to fail.
public class AutofacTest
{
[Test]
public void Test()
{
// (1) fooService not disposed: When resolved in another method
var (fooService, container) = GetContainerAndResolve();
// (2) fooService disposed: When resolved in unit test
//var container = GetContainerOnly();
//var fooService = container.Resolve<IFooService>();
fooService.UseFooService();
container.Dispose();
for (int i = 0; i < 10; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
dotMemory.Check(memory =>
{
Assert.AreEqual(0, memory.GetObjects(where => where.Type.Is<FooService>()).ObjectsCount);
});
}
private (IFooService, IContainer) GetContainerAndResolve()
{
var builder = new ContainerBuilder();
builder.RegisterType<Printer>().As<IFooPrinter>();
builder.RegisterType<FooService>().As<IFooService>();
var container = builder.Build();
var fooService = container.Resolve<IFooService>();
return (fooService, container);
}
private IContainer GetContainerOnly()
{
var builder = new ContainerBuilder();
builder.RegisterType<Printer>().As<IFooPrinter>();
builder.RegisterType<FooService>().As<IFooService>();
var container = builder.Build();
return container;
}
public interface IFooPrinter
{
void PrintFoo(string foo);
}
public interface IFooService
{
void UseFooService();
}
public class Printer : IFooPrinter
{
public void PrintFoo(string foo)
{
Console.WriteLine($"Print foo string: {foo}");
}
}
public class FooService : IFooService
{
private readonly IFooPrinter _fooPrinter;
public FooService(IFooPrinter fooSender)
{
_fooPrinter = fooSender;
}
public void UseFooService()
{
_fooPrinter.PrintFoo("Print from Service!");
}
}
}
Note that the GC.Collect()
is run multiple times to ensure that the finalizers are executed before the objects are reclaimed.
The amount of details in dotMemory profiler is substantial, therefore I modified the code to using dotMemory Unit. Upon unit test failure, the snapshot will produce a .dmw file for detailed analysis.
While I wish I had time to allocate free consulting time to troubleshoot your memory issues, I just don't have that time. You're going to have to do the deeper work on your own and come back with some proof it's Autofac, like what thing in Autofac you think is holding the references.
You're correct that the object does not implement IDisposable. However, that shouldn't matter because we expect the object, when resolved using the container, to be released upon disposing the container, allowing its memory to be reclaimed when GC is invoked. Now, it appears that the behavior is inconsistent when we resolve the object in a method or within a unit test.
Here are the updates:
Regarding the StackOverflow question, I didn't mention that it was posted by me. It was an old post (from 6 years ago) that I came across, which discussed the need to call GC multiple times in some instances.
If changing the return type fixes things, it sounds like the bug is in the way the compiler is handling that syntactic sugar. As noted, for transient objects that are not IDisposable, Autofac does not hold references at all, which is why I'm confused. I mentioned the difference in code from the SO question because the answer to the question provides some specific parameters to garbage collection that cause it to behave differently. That's worth a try.
We still need proof that this is Autofac. Thus far there's no evidence to that effect.
Since there's been no feedback here or proof this is an Autofac problem, I'm going to close the ticket. If we can see something concrete showing it's an Autofac issue we can reopen.
I'm encountering an issue with the Autofac container's disposal behavior. Specifically, when I resolve dependencies in another method or class, the
container.Dispose()
method doesn't seem to work as expected, leaving some objects undisposed. However, when I resolve dependencies directly within a unit test, it works correctly.Here's a simplified version of the code that illustrates the problem. I’m using dotMemory to capture two snapshots to check if fooService is disposed.
In our actual codebase, we resolve dependencies in a separate Instantiator class and return the main resolved item (e.g., fooService) along with the container. This approach seems to lead to issues with object disposal.
Here're the snapshots showing that (1) fooService is not disposed (survived)
(2) fooService properly disposed (dead)
Autofac version: 6.2.0