It has been interesting three months working on the lifetime story for MEF. Turned out to be bigger and more complex than any one could anticipate. As I walk you through what has been decided and implemented you may find things too trivial, but actually it was huge as key members in our team wanted something way different. Under this light I really considered the MEF/Lifetime a success as we â€“ the feature crew (Wes and Daniel) â€“ were able to demonstrate clearly the impacts of their proposed changes and how our proposal solved key scenarios.
Here is a summary of the top level decisions:
- We will continue to support singletons and transients (we renamed them to Shared and NonShared)
- We will continue to support disposable types (even when IDisposable is not part of the public contract of the service)
- The container will have the ownership of parts it instantiates (we will not transfer ownership to parts)
We now support three creation policies. They can be specified on the part level â€“ using the CompositionOptionsAttribute â€“ or on the import level, using the ImportAttribute. Both sides need to permissible in order to the dependency be satisfied.
- Shared: former Singleton. A single instance per lifetime domain (ie, container)
- NonShared: former Factory. Whenever an export is requested, a new instance is created.
- Any: defines that the part can function as Shared or NonShared, it would be up to the import side to define. If â€œAnyâ€ is specified on both sides, it will default to Shared.
Container and parts references
The CompositionContainer will not hold references to parts it creates, unless for the following cases:
1. The ComposablePart.RequiresDisposal returns true â€“ which for our default programming will be true only when it wraps an object instance that implements IDisposable
2. The part has one or more imports marked with AllowRecomposition=true. In this case, though, we hold conditional references, which means that while thereâ€™s an alive export instance the GC wonâ€™t collect the part instance.
3. Shared parts
For all other situations, parts are created, exports gathered and we drop the references leaving everything else to the GC.
One thing to note: if have NonShared disposable parts you might incur in memory bloat. There are two ways to avoid that, described below.
Disposing the container
Disposing the container will shutdown parts held (possibly disposing them, and disabling recomposition). Lazy exports wont work anymore. As any disposable type, we havenâ€™t engineered the container to be reusable after being disposed.
You can use container hierarchies in order to create scoped lifetime. For example, a web request is a great candidate to be mapped that way. However, as the container depends on Catalogs you will either have to filter the global catalog â€“ the one that the parent container has access to â€“ or handcraft a new catalog. Using the same catalog on the parent and on the child container wonâ€™t make much sense IMHO as then you start to have short lived Singletons.. and this doesnâ€™t feel right to me.
Check the Filtering Catalogs for more information on this.
Early release of resources
If you donâ€™t want to use container hierarchies, you can also release an object graph. This is a great solution for scoped operations that you control when it starts and ends.
Just use the method ReleaseExport exposed on the CompositionContainer class. It traverses the parts and its imports releases NonShared parts. It stops in Shared parts as we cannot touch them.
Lifetime of parts manually added to the container
When you add an object instance directly to the container you do not transfer its ownership to it â€“ at least not now, weâ€™re discussing and reviewing this behavior. However, adding a part manually will cause composition, so new parts may be created to satisfy your object dependencies. Who own them? The container. MEF is smart enough to shutdown them when you remove that object from the container.
Due to the fact that we support â€“ to some extend â€“ cycled object graphs, we cannot create a 100% deterministic disposal algorithm. Hence, we do not guarantee disposal ordering and urge you to follow a simple guideline:
â€œDo not use imports on your dispose method.â€
That means, do not use the things your part imported on your implementation of the Dispose method. Why? Because as we do not guarantee ordering, they might have been disposed when you try to use them.
Itâ€™s been a fun ride. Given the fact that we considered removing support for NonShared, transfer ownership of exports to imports and all sorts of options, Iâ€™m absolutely positive that we have made the right choices. Iâ€™ve learned that thereâ€™s no such thing as the perfect design for complex problems. In that case you have to strive for the right trade-offs and make sure you provide solutions for the key scenarios.