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 · 5 comments

Comments

Projects
None yet
2 participants
@peter-perot

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

This comment has been minimized.

Contributor

tillig commented Jul 7, 2017

Duplicate of autofac/Autofac#780

@peter-perot

This comment has been minimized.

peter-perot commented Jul 7, 2017

@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

This comment has been minimized.

Contributor

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

This comment has been minimized.

peter-perot commented Jul 11, 2017

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

This comment has been minimized.

Contributor

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.

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