Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with disposable singletons #15

Closed
peter-perot opened this issue Jul 7, 2017 · 7 comments
Closed

Issue with disposable singletons #15

peter-perot opened this issue Jul 7, 2017 · 7 comments

Comments

@peter-perot
Copy link

peter-perot commented Jul 7, 2017

Let's assume we have the following types:

public interface IMyInterface
{
}

public sealed class MyClass : IMyInterface, IDisposable
{
  public void Dispose()
  {
    Console.WriteLine("Disposing");
  }
}

When using Microsoft ASP.NET DependencyInjection the following code works correctly:

private static void MicrosoftDI_1()
{
  var sc = new ServiceCollection();
  var externallyOwned = new MyClass();

  sc.AddSingleton<IMyInterface>(externallyOwned);

  var sp = sc.BuildServiceProvider();

  var myObj = sp.GetRequiredService<IMyInterface>();

  ((IDisposable)sp).Dispose(); // myObj is _not_ disposed
}

The externally owned singleton is not disposed - that's correct.

The following code works correctly, too:

private static void MicrosoftDI_2()
{
  var sc = new ServiceCollection();

  sc.AddSingleton<IMyInterface>(_ => new MyClass());

  var sp = sc.BuildServiceProvider();

  var myObj = sp.GetRequiredService<IMyInterface>();

  ((IDisposable)sp).Dispose(); // myObj is disposed
}

Here the singleton myObj is disposed, because it is created inside of a lamdba factory function -
correct so far.

Now let's see what Autofac using the Microsoft ASP.NET DependencyInjection adapter is doing:

private static void AutofacDI_1()
{
  var sc = new ServiceCollection();
  var externallyOwned = new MyClass();

  sc.AddSingleton<IMyInterface>(externallyOwned);

  var cb = new ContainerBuilder();
  cb.Populate(sc);
  var ctnr = cb.Build();
  var sp = new AutofacServiceProvider(ctnr);

  var myObj = sp.GetRequiredService<IMyInterface>();

  ctnr.Dispose(); // myObj is disposed - this is not correct!
}

The externally owned singleton is disposed on container disposal - this is not correct.

When using a lambda factory function, Autofac is working correctly:

private static void AutofacDI_2()
{
  var sc = new ServiceCollection();

  sc.AddSingleton<IMyInterface>(_ => new MyClass());

  var cb = new ContainerBuilder();
  cb.Populate(sc);
  var ctnr = cb.Build();
  var sp = new AutofacServiceProvider(ctnr);

  var myObj = sp.GetRequiredService<IMyInterface>();

  ctnr.Dispose(); // myObj is disposed
}

The singleton is disposed as expected.

Used versions:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Autofac" version="4.6.0" targetFramework="net46" />
  <package id="Autofac.Extensions.DependencyInjection" version="4.1.0" targetFramework="net46" />
  <package id="Microsoft.Extensions.DependencyInjection" version="1.1.1" targetFramework="net46" />
  <package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="1.1.1" targetFramework="net46" />
</packages>
@tillig
Copy link
Member

tillig commented Jul 7, 2017

Duplicate of autofac/Autofac#780

@peter-perot
Copy link
Author

@tillig I don't think this is a duplicate of autofac/Autofac#780

Autofac.Extensions.DependencyInjection is an adapter for Autofac in order to act as a substitute for Microsoft ASP.NET DependencyInjection when using the conforming container pattern.

The problem here is: when you are using the original DI container from Microsoft the behavior is different compared to the implementation of the adapter.

I think the issue can simply be resolved by checking if the descriptor of the singleton service contains a ImplementationInstance. If this is the case the singleton should be registered as externally owned at the Autofac container builder.

@tillig
Copy link
Member

tillig commented Jul 7, 2017

It's a duplicate because when you're using the Autofac adapter the thing fulfilling requests is not the Autofac adapter it's the Autofac container. The shell implementations of IServiceProvider and so on in the Autofac.Extensions.DepdencyInjection are just proxies to Autofac functions. The issue you're pointing out is an issue of whether we're properly handling disposal of provided instances in core Autofac (which is issue autofac/Autofac#780) - it really doesn't have anything to do with Autofac.Extensions.DependencyInjection or Microsoft.Extensions.DependencyInjection.

To be clear, there will definitely be behavior differences between the Autofac DI container and the Microsoft DI container. We're not trying to behave identically - if you choose to use Autofac as your backing container, you're also choosing the Autofac behaviors. The difference in behavior you found is one of probably many; it just happens that we've been having a discussion about how Autofac should handle it (and, by virtue of that, how the Autofac.Extensions.DependencyInjection adapter will behave).

What you've stumbled into inadvertently is a fairly huge discussion where Microsoft has created a container (rather than just abstractions/interfaces) and developers somewhat assume if they switch backing containers that behavior will be identical - that there's a conforming container here. You can see it in issues and blog entries like this where many different container owners are concerned about that:

@peter-perot
Copy link
Author

Interesting - the behaviour described here is really quirky (the difference between registration at container level as opposed to registration at scope level with a different behavior on disposal).

So I understand your point.

But do you have an idea how to keep the externally owned instance (see my initial example) from getting disposed when the container is disposed? In Autofac I could use ExternallyOwned() on registration but what will I do when using the adapter?

@tillig
Copy link
Member

tillig commented Jul 11, 2017

The workaround now is to register directly with Autofac instead of the adapter.

For the adapter, we need to solve the behavior in the core container. We are using RegisterInstance in the adapter (which is correct) but it's not behaving consistently. Once figure out what core Autofac should do, that's what the adapter will also do. The adapter will behave like Autofac.

If you want to follow autofac/Autofac#780, that's where this is being discussed.

@HeikoOehme69
Copy link

From my perspective the issue comes from a gap in the overall design of dependency injection containers. That is not only an autofac issue. Often dependency containers work as a service registries and service factories at once. From the client side it is not possible to decide who is responsible for disposing an object. This detail is provided by the registration but not forwarded appropriately to the client. Or did I oversee something?

@peter-perot
Copy link
Author

From my perspective the issue comes from a gap in the overall design of dependency injection containers. That is not only an autofac issue. Often dependency containers work as a service registries and service factories at once. From the client side it is not possible to decide who is responsible for disposing an object. This detail is provided by the registration but not forwarded appropriately to the client. Or did I oversee something?

Disposable services are typically registered as scoped services, so the container is responsible for disposing them. Therefore I think an injected service should never be disposed by the instance that get the service injected. Only services that are created by an (injected) service factory should be disposed by the code that uses the factory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants