First of all, this blog post contains my own views and does not necessarily reflect the view of employer.
MEF is a complex small project, dealing with a big and complex problem. In our team a feature crew - a PM, dev and tester - owns a feature. We research, brainstorm, prototype to exhaustion, come up with real world scenarios, brainstorm some more and then finally a spec is produced (or finalized). I'm on the lifetime feature crew. Our goal is to decide and propose a solution on how MEF should deal with lifetime.
There is a broad range of possible solutions, from not supporting anything at all (which is a valid option) to support a full extensible (and possibly complex) lifetime story. Things to consider are:
- Being part of the core framework comes with responsabilities. All dogmas should be left on the door, and a solution should be compatible with how the rest of the framework (and its guidelines)
- MEF is not an full fledge IoC, and as such it shouldn't need to behave as one
- The final solution should translate into a predictable behavior for users (more on that below)
The rest of the post is me sharing (with authorization) some of our findings, some of tough calls to make, and kindly asking for feedback 🙂
In case you're not familiar, lifetime is a concept widely used on component frameworks, especially IoC containers. However it's not 100% adopted, and it might use different names. It defines the scope of accessibility and how (or better, if) an instance can be shared. Singleton is a common lifetime. So is Transient.
Windsor (and back to Apache Avalon/Excalibur) refers to them as lifestyle instead, and supports Singleton, Transient, Pooled, PerThread and PerWebRequest out of box. It's relatively easy to add a new one. Spring supports Singleton and Transient (which it calls Prototype). The Java version of spring brings extensibility points so you can add new ones. PicoContainer supports only Singletons - new lifetimes can be supported using child container (and carefully registering the components on correctly in the container hierarchy). Autofac supports both singleton and transient, and also relies on container hierarchies to support others.
Outside of the realm of IoC container you can still see similar concepts on OSGi, EJB, COM+ and .Net Remoting. Take the last as a more reachable example, the WellKnownObjectMode enum reads Singleton, SingleCall, which is essentially the same Singleton/Transient pair supported by some IoC containers.
All is good, but then you face the challenges that this simple concept brings with it.
Not all lifetimes are compatible with each other. You won't have problems as long as it goes bottom up, from most restricted lifetimes depending on things served but more broad lifetimes. For instance, it's OK to have a per-web-request component that has a dependency on a per-session lifetime, which itself has dependencies on singletons. The opposite is not true, though.
Lifetimes that maps to non-boundaries context
Singletons can be mapped to the life of the container. You shutdown the container and all singletons are shutdown too. Per-session has a clearly boundary (session starts/ends) and so does per-web-request.
What about Transients? They can be map to the container lifetime too, but would that mean that we only shutdown them when the container is disposed? That would lead to growing memory consuption in some apps scenarios. What about per-thread? Do we get a notification we a thread is about to start and when it's about to die?
This is not entirely related to lifetime, rather a common OOP problem. Usually you're forced to read the documentation to find out how you're suppose to deal with an object under a particular situation, as the API usually wont be able to tell you. Consider the following:
FileStream fs = ...
var processor = new FileProcessor(fs);
FileStream clearly needs to be disposed. By externally creating it and passing to some other object, do you implicitly transfer the ownership? Some will say yes, some will say no. Others will go with "depends".
IMHO, for the sample above you do not transfer ownership, so the creator (you) have to dispose it. However if the FileProcessor itself created the FileStream, than it would have the ownership and the obligation to dispose it. Yes, I know, we have finalizers, but the way I see they are safeguards more than anything else. I'll come back to this.
Fixing our code:
using(FileStream fs = ...)
var processor = new FileProcessor(fs);
The devil is in the details, though. What if FileProcessor is a long lived instance, that process the file in response to some external stimulus? In that case, the code above will early dipose a stream that the processor will need later.
Yeah, things are not as easy as they seem. You can come up with something that defines clear contracts, though:
var processor = new FileProcessor(new FileAccessor(new DirectoryInfo("./files")));
Depending on the FileAccessor API it would be more or less clear whose the ownership of file.
A stream is a relatively straingforward example as it's something that is clearly unshareable. Now consider an even worse situation:
var sender = new FtpSender();
var backupSvc = new BackupService(sender);
var docBrw = new DocumentUploaderService(sender);
FtpSender implements IDisposable, which closes the outgoing transfers and the main connection. Should the dependant services ever dispose them? Clearly no, as it's being shared. But how will the BackupService ever know it's shared? Same thing applies for DocumentUploaderService.
As someone who things conventions (and by consequence guidelines) tend to result in good things, I'd say that it would be safer to rely on a simple convetion: whoever created something has the obligation of destroying it. Obviously that will only apply to instances that have some sort of shutdown semantics like Stop, Close or Dispose.
Bringing all that to IoC containers. Windsor has always encouraged that. You don't care how things ended up on your constructor or properties, and you shouldn't care. In order words: <b>Thou shall shutdown them explicitly</b>.
Spring, however, deals with shutdown semantics for singleton, but leaves it up to you in the case of transients. I disagree with this as I find troublesome to have to know the lifetime of my dependencies and act accordingly - IMHO I shouldn't have to care - but I deeply understand their reasons. Here's why:
First, in order to shutdown short lived instances, you need to expose an operation to do that. Windsor and Ninject expose a Release method on the container interface. <i>What happens if the user doesn't call it? </i>
Secondly, imagine that you have instances that don't care for shutdown, but later on on the dependency chain there's a instance who cares. Guess what. You'll need to keep a chain of references in order to dispose that last one - what I like to call component burden (there is room for optimizations on the implementation algorithm, though). Why you need the chain? Because Release will be called on the first (root) instance.
So all in all, while it's easy to put an statement defining how ownership should be handled it does make things _way_ complex. There's a balance however, on what is the right behavior we should strive for, and what's a problem without a good solution that we shouldn't spend energy trying to fix as there is no fix good enough anyway.
What about the finalizer? Like I said, it's a good safe guard, but I rarely use it. You should be aware that instantiating an object that uses finalizer is more expensive. Also, in the call to finalizer I'm not supposed to reference other instances (ie dispose things other than wild unmanaged resource) as there is not invocation ordering guaranted. Maybe I'm spoilled by the fact that I've always relied on IoC container to do the proper shutdown for me. Implementing IStartable and IDisposable always meant that Windsor will call them, in the right time. Not having to rely on "what if" situations made my code simpler and allowed me to focus on other things. Boilerplate is for frameworks, my code should be about domain.
Now, I'm interested in hear from you basically two things:
- Do you often rely on shutdown semantics on your components?
- What do you fell MEF should support?