As requested, the code…
Usage after the code.
using ServiceStack.Caching;
using System;
using System.Threading.Tasks;
namespace Ludu.Base
{
/// <summary>
/// Base class for asynchronously caching data. After a cache-expiry, the data will be fetched asynchronously.
/// While this fethcing is busy, the originally cached data is returned.
/// </summary>
/// <typeparam name="T">Type held by the cache</typeparam>
public class AsyncCache<T> where T : class
{
private readonly Func<T> GetData;
public AsyncCache(Func<T> getData, TimeSpan? lifeTime = null, T initialData = null)
{
GetData = getData;
this.lifeTime = lifeTime ?? TimeSpan.FromMinutes(10);
if (initialData != null)
{
InMemoryData = initialData;
CurrentState = State.OnLine;
RefreshedOn = DateTime.UtcNow;
}
}
private enum State
{
Empty,
OnLine,
Refreshing
}
private T InMemoryData { get; set; }
private volatile State CurrentState = State.Empty;
private volatile object StateLock = new object();
private DateTime RefreshedOn = DateTime.MinValue;
private readonly TimeSpan lifeTime;
public T Data
{
get
{
switch (CurrentState)
{
case State.OnLine: // Simple check on time spent in cache vs lifetime
var timeSpentInCache = (DateTime.UtcNow - RefreshedOn);
if (timeSpentInCache > lifeTime)
{
lock (StateLock)
{
if (CurrentState == State.OnLine)
{
CurrentState = State.Refreshing;
Task.Factory.StartNew(Refresh);
}
}
}
break;
case State.Empty: // Initial load : blocking to all callers
lock (StateLock)
{
if (CurrentState == State.Empty)
{
InMemoryData = GetData(); // actually retrieve data
RefreshedOn = DateTime.UtcNow;
CurrentState = State.OnLine;
}
}
return InMemoryData;
}
return InMemoryData;
}
}
private void Refresh()
{
var dt = GetData(); // actually retrieve data from inheritor
lock (StateLock)
{
RefreshedOn = DateTime.UtcNow;
CurrentState = State.OnLine;
InMemoryData = dt;
}
}
public void Invalidate()
{
lock (StateLock)
{
RefreshedOn = DateTime.MinValue;
CurrentState = State.OnLine;
}
}
public static T Get(ICacheClient cache, string cacheKey, Func<T> getData)
{
return Get(cache, cacheKey, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(10), getData);
}
public static T Get(ICacheClient cache, string cacheKey, TimeSpan lifeTime, TimeSpan cacheTime, Func<T> getData)
{
var cachedData = cache.Get<AsyncCache<T>>(cacheKey);
if (cachedData == null)
{
lock (getData)
{
// double check - voorkomt dat de lock elke keer moet worden geplaatst
cachedData = cache.Get<AsyncCache<T>>(cacheKey);
if (cachedData == null)
{
cachedData = new AsyncCache<T>(getData, lifeTime);
cache.Set<AsyncCache<T>>(cacheKey, cachedData, cacheTime);
}
}
}
return cachedData.Data;
}
}
}
Usage:
public List<Tag> GetByIdCampaignCached(int idCampaign)
{
string cacheKey = UrnId.CreateWithParts("TagsByIdCampaign", idCampaign.ToString());
return AsyncCache<List<Tag>>.Get(cacheClient, cacheKey, TimeSpan.FromSeconds(5), TimeSpan.FromHours(1), () =>
{
return GetByIdCampaign(idCampaign);
});
}
This will cache the call to GetByIdCampaign for 5 seconds. Since getting these tags is costly - it takes about 2 seconds - a consecutive call will return the PREVIOUSLY requested data and thus limits the wait time after the cache expired. The second timespan (1 hour) is the time the cached object will remain active. After this time the whole caching object will be discarded and will need to be fully retrieved (initial performance hit)
To see this in action, see: http://www.ohnespass.de and http://www.zondergein.nl/