Search code examples
c#.netdependency-injectiondisposedryioc

How to handle disposal of objects when using DI/IoC container?


I am using DryIoc, but I think this is a general question applicable to any IoC container.

Consider a singleton service(Let's call it SingletonService). The SingletonService needs a new set of objects every time one of its functions is called. For simplicity let's say there is only one object and is called WorkerService. WorkerService will use unmanaged resources and therefor needs to be disposed.

I am unclear how to make sure that the WorkerService will be disposed as soon as it is not needed anymore. To have a reference to the container in SingletonService is a no go as far as I understand, because of the service locator anti-pattern.

One solution that comes to mind is to inject a WorkerServiceFactory into SingletonService. WorkerServiceFactory gets the container via injection. The factory then uses the container to get WorkerService and returns it to SingletonService. Is SingletonService the one responsible for disposing of WorkerService? Isn't there a guideline that states that objects created in the container should be disposed by the container? Alternatively I could use a scope but I am unclear how. If I were to create a new scope in SingletonService it would still contain a reference to the container(at least in DryIoc unless I am missing something). Creating the scope in the factory is meaningless since the scope would only be accessible by the factory.

How do you handle situations like this?

Edit 1:

I though about it a little more and came up with this:

using DryIoc;
using JetBrains.Annotations;

using var container = new Container();
container.Register<ISingletonService, SingletonService>(reuse: Reuse.Singleton);
container.Register<IWorkerService, WorkerService>(reuse: Reuse.Scoped);
container.Register<IWorkerServiceFactory, WorkerServiceFactory>(reuse: Reuse.Singleton);
var singleton = container.Resolve<ISingletonService>();
singleton.PrintData();
singleton.PrintData();
singleton.PrintData();
singleton.PrintData();
singleton.PrintData();


interface ISingletonService
{
    void PrintData();
}

class SingletonService(IWorkerServiceFactory workerFactory) : ISingletonService
{
    public void PrintData()
    {
        using var scope = workerFactory.Create(out var worker);
        Console.WriteLine(worker.GetData());
    }
}

interface IWorkerService
{
    string GetData();
}

class WorkerService : IWorkerService, IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("WorkerService was disposed");
    }

    public string GetData()
    {
        return "Hello Data";
    }
}

interface IWorkerServiceFactory
{
    [MustDisposeResource]
    IDisposable Create(out IWorkerService service);
}

class WorkerServiceFactory(IResolverContext ctx) : IWorkerServiceFactory
{
    public IDisposable Create(out IWorkerService service)
    {
        var scope = ctx.OpenScope();
        service = scope.Resolve<IWorkerService>();
        return scope;
    }
}

Essentially I am returning the WorkerService with an out parameter which allows me to return the scope created inside of my factory to the caller. This way SingletonService only cares about disposing the scope returned from the factory and the container handles everything else. Is this a sound approach?


Solution

  • This looks like an example of what Mark Seemann has named the Captive Dependency problem. That article explores the problem well, but, albeit via skim-reading, I cannot see any suggestions there on how to address it.

    I've used Castle.Windsor extensively in the past. It has a feature called the Typed Factory Facility. I'm not suggesting you start using Castle.Windsor for this feature, but I think it's a pattern that gives you a possible answer to your question. TBH, you've practically arrived at yourself anyway.

    It provides container-aware factory implementations using an interface-based convention. So it's essentially the same as your IWorkerServiceFactory. The key bit is the section on releasing the components you resolve via the factory. My perception of your current solution is that returning the container scope to the caller is giving the caller too much knowledge of the underlying infrastructure. The Typed Factory approach abstracts the lifespan management via the 'release/destroy' method on the interface. This way, the caller is responsible for the component's clean-up, but the mechanism is opaque. I would suggest doing something similar with your factory and encapsulating/hiding the underlying container mechanisms.

    It looks like the Typed Factory feature also has support for IDisposable, but this appears to operate at the factory level. I'm not sure how I'd implement my own equivalent of that, but that doesn't mean you should overlook it.

    So, something like:

    interface IWorkerServiceFactory
    {
        IWorkerService Create();
        void Release(IWorkerService workerService);
    }
    
    class WorkerServiceFactory(IResolverContext ctx) : IWorkerServiceFactory
    {
        public IWorkerService Create()
        {
            // I don't know DryIoc, so I'm guessing...
            return ctx.Resolve<IWorkerService>();
        }
    
        public void Release(IWorkerService workerService)
        {
            return ctx.Release(workerService);
        }
    }