Tuesday, June 17, 2014

StructureMap: time expiring objects cache

StructureMap is my favorite .NET's IoC container. It has a very nice API and is quite well extensible. One of the things I use its extensibility points for is to have my expensive objects cached for some time. Not a singleton, as the cached values are changing from time to time and I want to see those changes eventually. Also not a transient nor per-request instance, as filling the cache is expensive - let's say it's a web service call that takes several seconds to complete. There is no such object lifecycle provided by StructureMap. Let's fix it!

What I need is a custom lifecycle object, so that I can configure my dependencies almost as usual - instead of for example:

For<IDependency>().HybridHttpOrThreadLocalScoped()
    .Use<NotSoExpensiveDependency>();

I'll use my own lifecycle using more generic LifecycleIs DSL method:

For<IDependency>().LifecycleIs(new TimeExpiringLifecycle(secondsToExpire: 600))
    .Use<DependencyFromWebService>();

LifecycleIs expects me to pass ILifecycle implementation in. That interface is responsible for keeping a cache for the objects. Its responsibility is to decide where that cache is and how long does it live. In our case, all we need to do is to use "singleton-like" cache (MainObjectCache) and make sure it is invalidated after a given period of time. Easy as that!

This is how it looks like for StructureMap 2.6 family:

public class TimeExpiringLifecycle : ILifecycle
{
    private readonly long _secondsToExpire;
    private readonly IObjectCache _cache = new MainObjectCache();

    private DateTime _lastExpired;

    public TimeExpiringLifecycle(long secondsToExpire)
    {
        _secondsToExpire = secondsToExpire;
        Expire();
    }

    private void Expire()
    {
        _lastExpired = DateTime.Now;
        _cache.DisposeAndClear();
    }

    public void EjectAll()
    {
        _cache.DisposeAndClear();
    }

    public IObjectCache FindCache()
    {
        if (DateTime.Now.AddSeconds(-_secondsToExpire) >= _lastExpired)
            Expire();

        return _cache;
    }

    public string Scope
    {
        get { return GetType().Name; }
    }
}

And here is the same for StructureMap 3.0 (there were some breaking names changes etc.)

>public class TimeExpiringLifecycle : ILifecycle
{
    private readonly long _secondsToExpire;
    private readonly IObjectCache _cache = new LifecycleObjectCache();

    private DateTime _lastExpired;

    public TimeExpiringLifecycle(long secondsToExpire)
    {
        _secondsToExpire = secondsToExpire;
        _cache.DisposeAndClear();
    }

    private void Expire()
    {
        _lastExpired = DateTime.Now;
        _cache.DisposeAndClear();
    }

    public void EjectAll(ILifecycleContext context)
    {
        _cache.DisposeAndClear();
    }

    public IObjectCache FindCache(ILifecycleContext context)
    {
        if (DateTime.Now.AddSeconds(-_secondsToExpire) >= _lastExpired)
            Expire();

        return _cache;
    }

    public string Description
    {
        get 
        {
            return "Lifecycle for StructureMap that keeps the objects for the period of given seconds."; 
        }
    }
}

StructureMap is responsible for reading and writing the cache, constructing the objects etc. - we don't need to care about that stuff at all. The only thing we should remember is that although all the requests within 600 seconds will be served with the cached object, after that time one of the requests will finally encounter a cache miss and will need to create that expensive cache, bearing the cost within that request.

2 comments:

  1. In your experience with this approach, how does StructureMap respond if 2 requests come close to the same time and both encounter the "cache miss"? Do both requests attempt to re-build the expensive cache? Or does StructureMap handle this cleanly such that the first request re-builds the cache and the second request just uses the cached information after it's been made available?

    ReplyDelete
  2. Can you also show how to do the invalidate cache if from external source?, e.g. a webapi call to the endpoint exposed by the project just for this purpose.

    ReplyDelete

Note: Only a member of this blog may post a comment.