StructureMap is an excellent IoC tool; very powerful and easy to use. One thing that I think is lacking though is a way to create a custom lifecycle "strategy" (For lack of a better word). Currently you can specify a lifecycle and you can use one the hybrid lifecycles but this isn't sufficient in some circumstances. In our code base we have ASP.NET web pages, WCF services, Windows services and console applications (And who knows what next, MVC?) and we have one registry for all these applications. We're using NHibernate so we have different lifecycle needs for ISession in each type of application. Another side of a lifecycle is the end of it. We want to be able to explicitly end a lifecycle in our ASP.NET and WCF apps. So this is what I mean by a "strategy".

Solution

One thing that's really nice about StructureMap is that it's really easy to extend. Jeremy et al have done a great job of creating a codebase that's really easy to dive into and understand. So the following is a strategic lifecycle API that allows you to define a lifecycle strategy. I'll first explain how to use it and then further down I'll explain the code under the covers. Now I do want to give the disclaimer that I'm no StructureMap expert or anything, so there may be better or more "correct" ways to do this. If so, be sure to leave a comment if you know.

You can download the code here. The code that matters is in the StructureMapContrib project.

Lifecycle strategies can be defined with the LifecycleStrategiesAre() method which takes any number of ILifecycleStrategy parameters:

ObjectFactory.Configure(
    config => config.For<INumberGenerator>().
              LifecycleStrategiesAre(...).
              Use<NumberGenerator>());

This is simply a convenience extension method for the following:

ObjectFactory.Configure(
    config => config.For<INumberGenerator>().
              LifecycleIs(new StrategicLifecycle(...)).
              Use<NumberGenerator>());

StrategicLifecycle implements ILifecycle and contains the different lifecycle strategies. Now a LifecycleStrategy can take three pieces of information; first, a lambda that determines if the lifecycle is valid (If this is not specified it defaults to true). Second, a lambda that lazily creates the lifecycle. And third, a lambda that will deterministically dispose of the object cache (If this is not specified then the object cache is not deterministically disposed). The following is a strategy defined for ASP.NET pages:

new LifecycleStrategy(
    () => HttpContextLifecycle.HasContext() && HttpContext.Current.Handler is Page, 
    () => new HttpContextLifecycle(), 
    c => ((Page)HttpContext.Current.Handler).Unload += ((s,e) => c.DisposeAndClear())),

This strategy is only valid when there is a current HttpContext and when the handler is a Web.UI.Page. In this case we would use StructureMap's HttpContextLifecycle. And we want to deterministically dispose of the object cache by hooking into the page Unload event. We can define other strategies to create a full lifecycle strategy as follows:

ObjectFactory.Configure(
    config => config.For<INumberGenerator>().
              LifecycleStrategiesAre(
                  new LifecycleStrategy(
                        () => HttpContextLifecycle.HasContext() && HttpContext.Current.Handler is Page, 
                        () => new HttpContextLifecycle(), 
                        c => ((Page)HttpContext.Current.Handler).Unload += ((s,e) => c.DisposeAndClear())),

                  new LifecycleStrategy(
                        WcfOperationLifecycle.HasContext, 
                        () => new WcfOperationLifecycle(), 
                        c => OperationContext.Current.OperationCompleted += ((s, e) => c.DisposeAndClear())),

                  new LifecycleStrategy(() => new ThreadLocalStorageLifecycle())).
              Use<NumberGenerator>());

Here we have a strategy that defines lifecycles for ASP.NET pages, WCF services and a fallback or default strategy of thread local storage. BTW, the WcfOperatonLifecycle is explained here. Notice that the thread local storage doesn't specify if it is valid or an explicit dispose handler. Since it's a fall back strategy it is listed last and is always valid and the object cache will not be deterministically disposed. This registration looks a bit cluttered so I've encapsulated these strategies into their own classes which cleans things up a bit:

ObjectFactory.Configure(
    config => config.For<INumberGenerator>().
              LifecycleStrategiesAre(
                  new HttpContextAndPageLifecycleStrategy(),
                  new WcfOperationLifecycleStrategy(),
                  new LifecycleStrategy(() => new ThreadLocalStorageLifecycle())).
              Use<NumberGenerator>());

And that's it. Now below, if you're interested, we'll examine the code behind this.

Implementation

It all starts with the StrategicLifecycle class:

public class StrategicLifecycle : ILifecycle
{
    private IEnumerable<ILifecycleStrategy> _strategies;

    public StrategicLifecycle(params ILifecycleStrategy[] strategies)
    { _strategies = strategies; }

    public void EjectAll()
    { ResolveLifecycle().EjectAll(); }

    public IObjectCache FindCache()
    { return ResolveLifecycle().FindCache(); }

    public string Scope
    { get { return "StrategicLifecycle"; } }

    private ILifecycle ResolveLifecycle()
    {
        var lifecycle = (from strategy in _strategies
                         where strategy.IsValid()
                         select strategy.Lifecycle).FirstOrDefault();
        if (lifecycle == null) throw
            new Exception("Unable to find a suitable lifecycle Strategy.");
        return lifecycle;
    }
}

This lifecycle dynamically chooses a valid lifecycle by calling the IsValid() method on all the ILifecycleStrategy's passed in. The first one it finds is the winner.

Lifecycle strategies implement the ILifecycleStrategy interface which only has two members; IsValid() to determine it the lifecycle is valid in a particular circumstance and the Lifecycle property to return an instance of a lifecycle:

public interface ILifecycleStrategy
{
    bool IsValid();
    ILifecycle Lifecycle { get;}
}

This is the implementation of ILifecycleStrategy that I'm using:

public class LifecycleStrategy : ILifecycleStrategy
{
    private Func<bool> _isValid;
    private ILifecycle _lifecycle;
    private Func<ILifecycle> _create;
    private Action<IObjectCache> _dispose;

    public LifecycleStrategy(Func<ILifecycle> create, Action<IObjectCache> dispose) : this(() => true, create, dispose) { }
    public LifecycleStrategy(Func<bool> isValid, Func<ILifecycle> create) : this(isValid, create, null) { }
    public LifecycleStrategy(Func<ILifecycle> create) : this(() => true, create, null) { }

    public LifecycleStrategy(Func<bool> isValid, Func<ILifecycle> create, Action<IObjectCache> dispose)
    {
        _isValid = isValid;
        _create = create;
        _dispose = dispose;
    }

    public bool IsValid()
    {
        return _isValid();
    }

    public ILifecycle Lifecycle
    {
        get
        {
            if (_lifecycle == null)
                if (_dispose == null) _lifecycle = _create();
                else _lifecycle = new DisposableLifecycleProxy(_create(), _dispose);
            return _lifecycle;
        }
    }
}

This is simply a container for the lambdas passed in and for the lifecycle. The lifecycle is created lazily and cached. You'll notice that if a deterministic dispose lambda is passed, the lifecycle is wrapped in a DisposableLifecycleProxy class. Here is that class:

public class DisposableLifecycleProxy : ILifecycle
{
    private ILifecycle _lifecycle;
    private Action<IObjectCache> _dispose;
    private Dictionary<IObjectCache, IObjectCache> _objectCaches = 
        new Dictionary<IObjectCache, IObjectCache>();

    public DisposableLifecycleProxy(ILifecycle lifecycle, Action<IObjectCache> dispose)
    {
        _lifecycle = lifecycle;
        _dispose = dispose;
    }

    public void CacheDisposed(IObjectCache objectCache)
    {
        if (!_objectCaches.ContainsKey(objectCache)) return;
        lock (_objectCaches) { _objectCaches.Remove(objectCache); }
    }

    public void EjectAll() { _lifecycle.EjectAll(); }

    public IObjectCache FindCache()
    {
        IObjectCache objectCache = _lifecycle.FindCache();

        // This is here to ensure the close lambda is only executed once
        // per object cache. Not sure of a better way to handle this.
        if (!_objectCaches.ContainsKey(objectCache))
        {
            ObjectCacheProxy proxy = new ObjectCacheProxy(this, objectCache);
            lock (_objectCaches) { _objectCaches.Add(objectCache, proxy); }
            _dispose(proxy);
        }

        return _objectCaches[objectCache];
    }

    public string Scope { get { return _lifecycle.Scope; } }

    // ────────────────────────── Nested Types ──────────────────────────

    public class ObjectCacheProxy : IObjectCache, IDisposable
    {
        private IObjectCache _objectCache;
        private DisposableLifecycleProxy _lifecycleProxy;
        private bool _disposed;

        public ObjectCacheProxy(DisposableLifecycleProxy lifecycleProxy, IObjectCache objectCache)
        {
            _objectCache = objectCache;
            _lifecycleProxy = lifecycleProxy;
        }

        public void DisposeAndClear() 
        {
            _objectCache.DisposeAndClear(); 
            Dispose();
        }

        public void Dispose()
        {
            if (_disposed) return;

            _disposed = true;
            _lifecycleProxy.CacheDisposed(_objectCache);
        }

        public int Count { get { return _objectCache.Count; } }
        public void Eject(Type pluginType, Instance instance) { _objectCache.Eject(pluginType, instance); }
        public object Get(Type pluginType, Instance instance) { return _objectCache.Get(pluginType, instance); }
        public bool Has(Type pluginType, Instance instance) { return _objectCache.Has(pluginType, instance); }
        public object Locker { get { return _objectCache.Locker; } }
        public void Set(Type pluginType, Instance instance, object value) { _objectCache.Set(pluginType, instance, value); }
    }        
}

This is the part I'm not all that fond of. It's a bit too much acrobatics to do something really simple and I wasn't able to put any more effort into it because of time constraints. But it's the only way I could figure out to transparently control the lifetime of an object cache. Essentially the dispose lambda is called the first time the FindCache() method is called. This class maintains a dictionary of all the object caches it's called the dispose lambda on so that it only ever calls it once per object cache. Plus the object cache that is returned is wrapped in a proxy that will signal back that the object cache has been disposed and can be removed from the mapping. I wish there was something that indicated if the cache existed before you called FindCache(). That way you could tell if it was newly created and in this case call the explicit dispose lambda on it. For now this is my workaround.  

The rest of the classes in the StructureMapContrib project are just simple convenience classes and shouldn't require any explanation.