Microsoft DI - Are objects referenced within a factory implementation disposed?

300 views Asked by At

Are objects that are referenced, not created, within a factory implementation disposed by the container? See code below:

services.AddTransient(c => OwinContext.ServiceObject);

Will ServiceObject, which implements IDisposable, be disposed by the container considering it isn't created (i.e. new ServiceObject)?

ServiceObject is currently registered as Scoped, but we are getting ObjectDisposedException on the rare occasion. I'm guessing it's getting disposed within OWIN sometimes before our services are able to use it, which is why I was hoping of making it Transient but I'm worried the container will dispose of it more frequently.

1

There are 1 answers

1
Steven On BEST ANSWER

Disposable transient registrations are tracked by the container and disposed when their scope ends.

The Microsoft documentation is not always clear about this, as this documentation seems to suggest that "The framework does not dispose of the services automatically" that are "not created by the service container." Although the documentation is not incorrect, as it primarily talks about the registration of instances through the AddSingleton<T>(T instance) extension method — it is misleading because it doesn't hold for:

  • AddSingleton<T>(Func<IServiceProvider, T>),
  • AddScoped<T>(Func<IServiceProvider, T>), and
  • AddTransient<T>(Func<IServiceProvider, T>).

This statement can be easily verified using the following program:

using Microsoft.Extensions.DependencyInjection;

var disposable = new FakeDisposable();

var services = new ServiceCollection();

services.AddTransient(c => disposable);

var provider = services.BuildServiceProvider(validateScopes: true);

using (var scope = provider.CreateScope())
{
    scope.ServiceProvider.GetRequiredService<FakeDisposable>();
}

public class FakeDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine("Disposed");
}

Output:

Disposed

Conclusion: Yes, transient registrations for disposable objects are disposed of by the container.

There will be little difference between making this registration Transient or Scoped. In both cases the object will get disposed when the scope ends.

In the case of a Transient registration, though, you'll start to see the disposable get disposed of multiple times in case it gets injected multiple times. For instance:

using (var scope = provider.CreateScope())
{
    scope.ServiceProvider.GetRequiredService<FakeDisposable>();
    scope.ServiceProvider.GetRequiredService<FakeDisposable>();
    scope.ServiceProvider.GetRequiredService<FakeDisposable>();
}

Output:

Disposed
Disposed
Disposed

From reliability, however, it's better to stick with a Scoped registration, instead of Transient. This is because MS.DI will prevent Scoped registrations from being injected into Singleton consumers (in case the Service Provider is created by calling BuildServiceProvider(validateScopes: true)). In case your ServiceContext would get injected into a Singleton, it causes it to become a Captive Dependency and keep referenced (and likely used) by that Singleton, long after it got disposed of.

The most likely reason you are getting those ObjectDisposedExceptions is because Owin tries to use the ServiceContext after your (web request) scope is disposed.

The ServiceContext object is likely being controlled and disposed of by OWIN, which doesn't make it a good candidate to be disposed of by the container. But here's the problem: MS.DI will always try to dispose of Transient and Scoped registrations and the only way to prevent this from happening is to not register your ServiceContext.

The solution, therefore, is to wrap it in a "provider" object of some sort. For instance:

// New abstraction
public interface IServiceObjectProvider
{
    object ServiceObject { get; }
}

// Implementation part of your Composition Root (see: https://mng.bz/K1qZ)
public class AmbientOwinServiceObjectProvider : IServiceObjectProvider
{
    public object ServiceObject => OwinContext.ServiceObject;
}

// Registration:
services.AddScoped<IServiceObjectProvider, AmbientOwinServiceObjectProvider>();

// Usage:
public class MyController : Controller
{
    private readonly IServiceObjectProvider provider;

    public MyController(IServiceObjectProvider provider)
    {
        // Only store the dependency here: don't use it,
        // see: https://blog.ploeh.dk/2011/03/03/InjectionConstructorsshouldbesimple/
        this.provider = provider;
    }

    public string Index()
    {
        var so = this.provider.ServiceObject;
        // Do something with the Service object
    }
}