29

I'm a little bit confused about Dispose() methods in IDisposable implementations with Autofac usage

Say I have a certain depth to my objects:

  • Controller depends on IManager;
  • Manager depends on IRepository;
  • Repository depends on ISession;
  • ISession is IDisposable.

This leads to the following object graph:

new Controller(
    new Manager(
        new Repository(
            new Session())));

Do I need to make my Manager and Repository implement IDisposable as well and call Manager.Dispose() in Controller, Repository.Dispose() in Manager, etc. or will Autofac automagically know which objects in my call stack properly need to be disposed? Controller object is already IDisposable as it derives from base ASP.NET Web API controller

Steven
  • 166,672
  • 24
  • 332
  • 435
Igorek
  • 15,716
  • 3
  • 54
  • 92

1 Answers1

53

The general rule of resources is that:

He who owns the resource is responsible of disposing of it.

This means that if a class owns a resource, it should either dispose of it in the same method that it created it in (in which case the disposable is called an ephemeral disposable), or if that’s not possible, this generally means that the owning class must implement IDisposable, so it can dispose of the resource in its Dispose method.

But it is important to note that, in general, a class should only own a resource if it is responsible of its creation. When a resource is injected, however, it means that this resource already existed before the consumer did. The consumer didn't create the resource and should, therefore, not dispose of it. Although you can pass on the ownership of the resource to the consumer (and communicate this in the class’s documentation that ownership is passed on), in general you should not pass on the ownership, because this complicates your code and makes the application fragile.

Although the strategy of transferring ownership of objects might make sense in some cases, for instance for types that are part of a reusable API (like System.IO.StreamReader), it not a good idea when dealing with components that are part of your object graph. I’ll explain why below.

So even if your Controller depends on services that need to be disposed of, your controller should not dispose of them:

  • Because the consumer did not create such dependency, it has no idea what the expected lifetime of its dependency is. It could be very well that the dependency should outlive the consumer. Letting the consumer dispose of that dependency will, in that case, cause a bug in your application, because the next controller will get an already disposed of dependency, and this will cause an ObjectDisposedException to be thrown.
  • Even if the lifestyle of the dependency equals that of the consumer, disposing of it is still problematic, because this prevents you from easily replacing that component for one that might have a longer lifetime in the future. Once you replace that component for a longer-lived component, you will have to go through all it consumers, possibly causing sweeping changes throughout the application. In other words, you will be violating the Open/closed principle by doing that–it should be possible to add or replace functionality without making sweeping changes instead.
  • If your consumer is able to dispose of its dependencies, this means that you have to implement IDisposable on their abstractions. This means such abstraction is leaking implementation details–that’s a violation of the Dependency Inversion Principle. You are leaking implementation details when implementing IDisposable on an abstraction, because it is very unlikely that every implementation of that abstraction needs deterministic disposal and so you defined the abstraction with a certain implementation in mind. The consumer should not have to know anything about the implementation, whether it needs deterministic disposal or not.
  • Letting that abstraction implement IDisposable also causes you to violate the Interface Segregation Principle, because the abstraction now contains an extra method (i.e. Dispose), that not all consumers need to call. They might not need to call it, because –as I mentioned– the resource might outlive the consumer. Letting it implement IDisposable in that case is dangerous, because anyone can call Dispose on it causing the application to break. If you are more strict about testing, this also means you will have to test a consumer for not calling the Dispose method. This will cause extra test code. This is code that needs to be written and maintained.

Instead, you should only place IDisposable on the implementation. This frees any consumer of the abstraction from the doubts whether it should or shouldn't call Dispose (because there is no Dispose method to call on the abstraction).

Because only the component implements IDisposable and only your Composition Root creates components, it is the Composition Root that is responsible of its disposal. In case your DI container creates this resource, it should also dispose of it. DI Containers like Autofac will actually do this for you. You can easily verify this. In case you wire your object graphs without the use of a DI Container (a.k.a. Pure DI), it means that you will have to dispose of those objects in your Composition Root yourself.

Considering the object graph given in your question, a simplistic code example that demonstrates both resolve (i.e. composing) and release (i.e. dispose of) would like like this:

// Create disposable component and hold reference to it
var session = new Session();

// create the complete object graph including the disposable
var controller =
    new Controller(
        new Manager(
            new Repository(
                session)));

// use the object graph
controller.TellYoMamaJoke();

// Clean up resources
session.Dispose();

Of course this example ignores complicating factors such as implementing deterministic cleanup, integration with application frameworks, and the use of DI Containers, but hopefully this code helps painting a mental model.

Note that this design change makes your code simpler. Implementing IDisposable on the abstraction and letting consumers dispose of their dependencies will cause IDisposable to spread out through your system like a virus and pollutes your code base. It spreads out, because for any abstraction, you can always think of an implementation that needs to clean up its resources, so you will have to implement IDisposable on every abstraction. This means that every implementation that takes one or more dependencies also has to implement IDisposable as well, and this cascades up the object graph. This adds a lot of code and unnecessary complexity to each class in your system.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • I should have clarified that I meant a case where ownership is transferred upon injection. I do not know if this is true with Autofac. – John Saunders May 17 '15 at 18:24
  • I agree it would be a bad design. I don't know whether Autofac has a bad design. I should have made that more clear. – John Saunders May 17 '15 at 19:15
  • 1
    In that case, my answer has little value, so I have deleted it. You may want to edit your answer to accomodate that. – John Saunders May 17 '15 at 19:22
  • @Steven Thank you for great explanation! Can you assist with the follow up question @ http://stackoverflow.com/questions/30294166 – Igorek May 18 '15 at 02:14
  • @Igorek: I'm sorry, but your new question goes too deep into Autofac's details. I can't help you with that. I'm not that familiar with Autofac. – Steven May 18 '15 at 08:14
  • Great explanation! Could you clarify what exactly do you mean by the "reusable API" and why are you contrasting it with the situation when the component is a part of my own object graph? – yaskovdev Sep 21 '22 at 09:02
  • 1
    A reusable library or API is something that is used by third parties that you as writer have no control over. Everything that Microsoft ships as part of the .NET framework is a reusable library. Every NuGet package ever created is a reusable library. This in contrast for libraries that are part of a single solution. Those are under your control and can be refactored at will. Reusable libraries need very special design to be as usable as possible. This is a completely different ballgame than creating it yourself. That's why other rules might apply compared to application components. – Steven Sep 21 '22 at 09:45
  • @Steven, thank you for the clarification. Could you give a concrete example when does the ownership transferring make sense in case of a library? For example, `System.IO.StreamReader` forces me to transfer the ownership of the `Stream` object that I pass to it and disposes of the object by itself. At first glance, it looks like a poor design decision. Why is it better than keeping the responsibility of disposing of the `Stream` on whoever created the `StreamReader` instance to follow the "who owns the resource is responsible of disposing of it" rule? – yaskovdev Nov 14 '22 at 13:19
  • 1
    As I wrote my answer, it seems like if I'm agreeing with the design of `StreamReader`, but that is actually not the case. I myself tripped to many times on this implicit behavior of `StreamReader` that I wished Microsoft didn't use that design. But still, when designing an API of a reusable API you are trying to make "simple use cases easy and complex use cases possible." With that guidance, when the API at hand is a often used part and used in "simple use cases", you might want to make it easy for starting developers to fall in "the pit of success". That might justify such design. – Steven Nov 14 '22 at 15:06
  • 1
    But still, as an API designer you also have to think about the bigger picture and if when you are providing two methods of managing ownership, this might also be more confusing in the long run and makes it hard for starting developers to know when they implicitly transfer ownership and when not. I would personally prefer making the transfer of ownership very explicit instead of the implicit model that `StreamReader` uses, but in some cases it implicit ownership *might* make sense. But I have a hard time to provide you with an example where it makes sense for me. – Steven Nov 14 '22 at 15:07