Cache Infrastucture
/// <summary>
/// <see cref="ICache"/> implementation which does nothing
/// </summary>
/// <remarks>
/// Used when real caches are unavailable or disabled
/// </remarks>
public class NullCache : CacheBase
{
public NullCache(ICacheConfiguration cacheConfiguration)
: base(cacheConfiguration)
{
}
public override CacheType CacheType
{
get { return CacheType.Null; }
}
protected override void InitialiseInternal()
{
}
protected override void SetInternal(string key, object value)
{
}
protected override void SetInternal(string key, object value, DateTime expiresAt)
{
}
protected override void SetInternal(string key, object value, TimeSpan validFor)
{
}
protected override object GetInternal(string key)
{
return null;
}
protected override void RemoveInternal(string key)
{
}
protected override bool ExistsInternal(string key)
{
return false;
}
}
public interface ICache
{
/// <summary>
/// Returns the type of the cache
/// </summary>
CacheType CacheType { get; }
/// <summary>
/// Performs initialisation tasks required for the cache implementation
/// </summary>
void Initialise();
/// <summary>
/// Insert or update a cache value
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
void Set(string key, object value);
/// <summary>
/// Insert or update a cache value with an expiry date
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expiresAt"></param>
void Set(string key, object value, DateTime expiresAt);
/// <summary>
/// Insert or update a cache value with a fixed lifetime
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="validFor"></param>
void Set(string key, object value, TimeSpan validFor);
/// <summary>
/// Retrieve a value from cache
/// </summary>
/// <param name="key"></param>
/// <returns>Cached value or null</returns>
object Get(Type type, string key);
/// <summary>
/// Retrieve a typed value from cache
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
T Get<T>(string key);
/// <summary>
/// Removes the value for the given key from the cache
/// </summary>
/// <param name="key"></param>
void Remove(string key);
/// <summary>
/// Returns whether the cache contains a value for the given key
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
bool Exists(string key);
}
public enum CacheType
{
/// <summary>
/// No cache type set
/// </summary>
Null = 0,
Memory,
Http,
//AppFabric,
//Memcached,
//AzureTableStorage,
//Disk
}
[Trait("CacheTests", "CacheQueryHandlerDecorator")]
public class CacheQueryHandlerDecoratorTests : TestBase
{
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_cache_factory_getting_caheType_defined_on_cache_attribute(
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
sut.Handle(query);
cacheFactoryMock.Verify(handler => handler.GetCache(CacheType.Http), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_get_on_cache_handler_if_cache_factory_returns_non_null_cache(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Memory);
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
sut.Handle(query);
cacheMock.Verify(handler => handler.Get<FakeResult>(It.IsAny<string>()), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handle_if_cache_factory_returns_null(
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => null);
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handler_if_cache_factory_returns_null_cache_type(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Null);
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handler_if_cache_is_disabled_on_query_cache_attribute(
[Frozen] Mock<IQueryHandler<FakeCacheDisabledQuery, FakeResult>> handlerMock,
FakeCacheDisabledQuery query,
CacheQueryHandlerDecorator<FakeCacheDisabledQuery, FakeResult> sut)
{
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handler_if_cache_is_globally_disabled(
[Frozen] Mock<IQueryHandler<FakeQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheConfiguration> cacheConfigMock,
FakeQuery query,
CacheQueryHandlerDecorator<FakeQuery, FakeResult> sut)
{
cacheConfigMock.Setup(configuration => configuration.Enabled).Returns(false);
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handler_if_cache_returns_null(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheMock.Setup(cache => cache.Get<FakeResult>(It.IsAny<string>())).Returns(() => null);
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_call_handler_if_result_is_not_decorated_with_cache_attribute(
[Frozen] Mock<IQueryHandler<FakeQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheConfiguration> cacheConfigMock,
FakeQuery query,
CacheQueryHandlerDecorator<FakeQuery, FakeResult> sut)
{
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Once());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_not_call_cache_if_cache_factory_returns_null_cache_type(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Null);
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
sut.Handle(query);
cacheMock.Verify(handler => handler.Get<FakeResult>(It.IsAny<string>()), Times.Never());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_not_call_cache_if_cache_is_diabled_on_query_cache_attribute(
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeCacheDisabledQuery query,
CacheQueryHandlerDecorator<FakeCacheDisabledQuery, FakeResult> sut)
{
sut.Handle(query);
cacheFactoryMock.Verify(handler => handler.GetCache(It.IsAny<CacheType>()), Times.Never());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_not_call_cache_if_cache_is_globally_disabled(
[Frozen] Mock<IQueryHandler<FakeQuery, FakeResult>> handlerMock,
[Frozen] Mock<ICacheConfiguration> cacheConfigMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
FakeQuery query,
CacheQueryHandlerDecorator<FakeQuery, FakeResult> sut)
{
cacheConfigMock.Setup(configuration => configuration.Enabled).Returns(false);
sut.Handle(query);
cacheFactoryMock.Verify(handler => handler.GetCache(It.IsAny<CacheType>()), Times.Never());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_not_call_handler_if_cache_returns_non_null_object(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Memory);
cacheMock.Setup(cache => cache.Get<FakeResult>(It.IsAny<string>())).Returns(() => new FakeResult());
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Never());
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_return_cached_object(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
var expected = new FakeResult();
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Memory);
cacheMock.Setup(cache => cache.Get<FakeResult>(It.IsAny<string>())).Returns(() => expected);
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
var actual = sut.Handle(query);
handlerMock.Verify(handler => handler.Handle(query), Times.Never());
Assert.Equal(expected, actual);
}
[Theory, AutoMoqDataCacheConfig]
public void CacheDecorator_should_cache_handler_result_if_not_null(
[Frozen] Mock<ICache> cacheMock,
[Frozen] Mock<ICacheFactory> cacheFactoryMock,
[Frozen] Mock<IQueryHandler<FakeCacheQuery, FakeResult>> handlerMock,
FakeCacheQuery query,
CacheQueryHandlerDecorator<FakeCacheQuery, FakeResult> sut)
{
var expected = new FakeResult();
cacheMock.Setup(cache => cache.CacheType).Returns(() => CacheType.Memory);
cacheMock.Setup(cache => cache.Get<FakeResult>(It.IsAny<string>())).Returns(() => null);
cacheFactoryMock.Setup(factory => factory.GetCache(It.IsAny<CacheType>())).Returns(() => cacheMock.Object);
handlerMock.Setup(handler => handler.Handle(query)).Returns(expected);
sut.Handle(query);
cacheMock.Verify(cache => cache.Set(It.IsAny<string>(), expected, It.IsAny<TimeSpan>()), Times.Once());
}
}
public class AutoMoqDataCacheConfigAttribute : AutoDataAttribute
{
public AutoMoqDataCacheConfigAttribute()
: base(new Fixture()
.Customize(new MoqCacheConfigurationCustomization()))
{
}
}
public class MoqCacheConfigurationCustomization : CompositeCustomization
{
public MoqCacheConfigurationCustomization()
: base(new CacheConfigurationCustomization(),
new AutoMoqCustomization())
{
}
private class CacheConfigurationCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new CacheConfigurationSpecimenBuilder());
}
}
}
public class CacheConfigurationSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
var pi = request as ParameterInfo;
if (pi == null)
{
return new NoSpecimen(request);
}
if (pi.ParameterType != typeof(ICacheConfiguration))
{
return new NoSpecimen(request);
}
var mockCacheConfig = new Mock<ICacheConfiguration>();
mockCacheConfig.Setup(configuration => configuration.Enabled).Returns(true);
mockCacheConfig.Setup(configuration => configuration.Targets).Returns(new CacheTargetCollection());
mockCacheConfig.Setup(configuration => configuration.Profiles).Returns(new CacheProfileCollection());
mockCacheConfig.Setup(configuration => configuration.DefaultCacheType).Returns(CacheType.Null);
mockCacheConfig.Setup(configuration => configuration.DefaultCacheName).Returns("Think.Cache");
mockCacheConfig.Setup(configuration => configuration.DefaultDuration).Returns(60);
return mockCacheConfig.Object;
}
}
[Cache(Disabled = true)]
public class FakeCacheDisabledQuery : IQuery<FakeResult>
{
}
[Cache(CacheType = CacheType.Http)]
public class FakeCacheQuery : IQuery<FakeResult>
{
}
public class FakeQuery : IQuery<FakeResult>
{
}
public class FakeResult
{
}
public class CacheQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult> where TResult : class where TQuery : IQuery<TResult>
{
private readonly ICacheConfiguration _cacheConfiguration;
private readonly ICacheFactory _cacheFactory;
private readonly CacheKeyBuilder _cacheKeyBuilder;
private readonly IQueryHandler<TQuery, TResult> _handler;
public CacheQueryHandlerDecorator(IQueryHandler<TQuery, TResult> handler, ICacheConfiguration cacheConfiguration, CacheKeyBuilder cacheKeyBuilder, ICacheFactory cacheFactory)
{
_cacheConfiguration = cacheConfiguration;
_cacheKeyBuilder = cacheKeyBuilder;
_cacheFactory = cacheFactory;
_handler = handler;
}
public TResult Handle(TQuery query)
{
//if caching is disabled leave
if (!_cacheConfiguration.Enabled)
{
return _handler.Handle(query);
}
//get the cache settings from the attribute & config:
var cacheAttribute = GetCacheSettings(query);
//if no cache attribute set leave
if (cacheAttribute == null)
{
return _handler.Handle(query);
}
if (cacheAttribute.Disabled)
{
return _handler.Handle(query);
}
//if there is no cache provider
var cache = _cacheFactory.GetCache(cacheAttribute.CacheType);
if (cache == null || cache.CacheType == CacheType.Null)
{
return _handler.Handle(query);
}
InstrumentCacheRequest(query);
var cacheKey = _cacheKeyBuilder.GetCacheKey(query);
var cachedValue = cache.Get<TResult>(cacheKey);
if (cachedValue == null)
{
InstrumentCacheMiss(query);
//call intended method
var result = _handler.Handle(query);
if (result != null)
{
var lifespan = cacheAttribute.Lifespan;
if (lifespan.TotalSeconds > 0)
{
cache.Set(cacheKey, result, lifespan);
}
else
{
cache.Set(cacheKey, result);
}
}
return result;
}
InstrumentCacheHit(query);
return cachedValue;
}
private CacheAttribute GetCacheSettings(TQuery query)
{
//get the cache attribute & check if overridden in config:
var cacheAttribute = query.GetType().GetCustomAttribute<CacheAttribute>();
if (cacheAttribute != null)
{
var cacheKeyPrefix = _cacheKeyBuilder.GetCacheKeyPrefix(query);
var targetConfig = _cacheConfiguration.Targets[cacheKeyPrefix];
if (targetConfig != null)
{
cacheAttribute.Disabled = !targetConfig.Enabled;
cacheAttribute.Days = targetConfig.Days;
cacheAttribute.Hours = targetConfig.Hours;
cacheAttribute.Minutes = targetConfig.Minutes;
cacheAttribute.Seconds = targetConfig.Seconds;
cacheAttribute.CacheType = targetConfig.CacheType;
}
else
{
//if no target is set try and load the profile from configuration
if (cacheAttribute.ProfileName.IsNotNullOrEmpty())
{
var cacheProfile = _cacheConfiguration.Profiles[cacheAttribute.ProfileName];
cacheAttribute.Days = cacheProfile.Days;
cacheAttribute.Hours = cacheProfile.Hours;
cacheAttribute.Minutes = cacheProfile.Minutes;
cacheAttribute.Seconds = cacheProfile.Seconds;
cacheAttribute.CacheType = cacheProfile.CacheType;
}
}
//if no duration is set get the default global value from configuration
if (cacheAttribute.NoDurationSet())
{
cacheAttribute.Seconds = _cacheConfiguration.DefaultDuration;
}
//if no cache type is set get the global default from configuration
if (cacheAttribute.CacheType == CacheType.Null)
{
cacheAttribute.CacheType = _cacheConfiguration.DefaultCacheType;
}
return cacheAttribute;
}
return null;
}
private void InstrumentCacheHit(TQuery query)
{
//add instrumentation for cache hit here
}
private void InstrumentCacheMiss(TQuery query)
{
//add instrumentation for cache miss here
}
private void InstrumentCacheRequest(TQuery query)
{
//add instrumentation for cache call here
}
}
public class CacheKeyBuilder
{
private readonly ICacheConfiguration _cacheConfiguration;
public CacheKeyBuilder(ICacheConfiguration cacheConfiguration)
{
_cacheConfiguration = cacheConfiguration;
}
public string GetCacheKeyPrefix(object query)
{
var cacheKeyPrefix = query.GetType().Name;
this.Log().DebugFormat("CacheKeyBuilder.GetCacheKeyPrefix - returned {0}", cacheKeyPrefix);
return cacheKeyPrefix;
}
public string GetCacheKey(object query)
{
var prefix = GetCacheKeyPrefix(query);
var key = JsonConvert.SerializeObject(query);
var hashedKey = HashCacheKey(key);
if (!_cacheConfiguration.HashPrefixInCacheKey)
{
hashedKey = string.Format("{0}_{1}", prefix, hashedKey);
}
this.Log().DebugFormat("CacheKeyBuilder.GetCacheKey - returned {0}", hashedKey);
return hashedKey;
}
private static string HashCacheKey(string cacheKey)
{
//hash the string as a GUID:
byte[] hashBytes;
using (var provider = new MD5CryptoServiceProvider())
{
var inputBytes = Encoding.Default.GetBytes(cacheKey);
hashBytes = provider.ComputeHash(inputBytes);
}
return new Guid(hashBytes).ToString();
}
}
public interface ICacheFactory
{
ICache GetCache(CacheType type);
}
public class CacheFactory : ICacheFactory
{
private readonly IEnumerable<ICache> _caches;
private readonly ICacheConfiguration _cacheConfiguration;
public CacheFactory(IEnumerable<ICache> caches, ICacheConfiguration cacheConfiguration)
{
_caches = caches;
_cacheConfiguration = cacheConfiguration;
}
public ICache GetCache(CacheType type)
{
ICache cache = new NullCache(_cacheConfiguration);
try
{
var match = (from c in _caches
where c.CacheType == type
select c).LastOrDefault();
if (match != null)
{
match.Initialise();
cache = match;
}
}
catch (Exception ex)
{
this.Log().WarnFormat("Failed to instantiate cache of type: {0}, using null cache. Exception: {1}", type, ex);
cache = new NullCache(_cacheConfiguration);
}
return cache;
}
}
/// <summary>
/// Base class for <see cref="ICache" /> implementations
/// </summary>
public abstract class CacheBase : ICache
{
protected readonly ICacheConfiguration _configuration;
protected CacheBase(ICacheConfiguration configuration)
{
_configuration = configuration;
}
protected virtual bool ItemsNeedSerializing
{
get
{
return false;
}
}
public abstract CacheType CacheType { get; }
public void Initialise()
{
try
{
InitialiseInternal();
}
catch (Exception ex)
{
this.Log().Error(string.Format("CacheBase.Initialise - failed, NullCache will be used. CacheName: {0}", _configuration.DefaultCacheName), ex);
}
}
public void Set(string key, object value)
{
try
{
value = PreProcess(value);
SetInternal(key, value);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Set - failed, item not cached. Message: {0}", ex.Message);
}
}
void ICache.Set(string key, object value, DateTime expiresAt)
{
try
{
value = PreProcess(value);
SetInternal(key, value, expiresAt);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Set - failed, item not cached. Message: {0}", ex.Message);
}
}
public void Set(string key, object value, TimeSpan validFor)
{
try
{
value = PreProcess(value);
SetInternal(key, value, validFor);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Set - failed, item not cached. Message: {0}", ex.Message);
}
}
public T Get<T>(string key)
{
return (T) Get(typeof (T), key);
}
public object Get(Type type, string key)
{
object item = null;
try
{
item = GetInternal(key);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Get - failed, item not cached. Message: {0}", ex.Message);
}
return PostProcess(type, item);
}
public void Remove(string key)
{
try
{
RemoveInternal(key);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Remove - failed, item not cached. Message: {0}", ex.Message);
}
}
public bool Exists(string key)
{
var exists = false;
try
{
exists = ExistsInternal(key);
}
catch (Exception ex)
{
this.Log().WarnFormat("CacheBase.Exists - failed, item not cached. Message: {0}", ex.Message);
}
return exists;
}
protected abstract bool ExistsInternal(string key);
protected abstract object GetInternal(string key);
protected abstract void InitialiseInternal();
protected abstract void RemoveInternal(string key);
protected abstract void SetInternal(string key, object value);
protected abstract void SetInternal(string key, object value, DateTime expiresAt);
protected abstract void SetInternal(string key, object value, TimeSpan validFor);
private object PostProcess(Type type, object value)
{
//not doing anything at the moment
return value;
}
private object PreProcess(object value)
{
//not doing anything at the moment
return value;
}
}
[AttributeUsage(AttributeTargets.Class,AllowMultiple = false, Inherited = false)]
public class CacheAttribute : Attribute
{
/// <summary>
/// The name of the cache profile to be used
/// </summary>
public string ProfileName { get; set; }
/// <summary>
/// Lifespan of the response in the cache
/// </summary>
public TimeSpan Lifespan
{
get { return new TimeSpan(Days, Hours, Minutes, Seconds); }
}
/// <summary>
/// Whether caching is enabled
/// </summary>
public bool Disabled { get; set; }
/// <summary>
/// Days the element to be cached should live in the cache
/// </summary>
public int Days { get; set; }
/// <summary>
/// Hours the element to be cached should live in the cache
/// </summary>
public int Hours { get; set; }
/// <summary>
/// Minutes the element to be cached should live in the cache
/// </summary>
public int Minutes { get; set; }
/// <summary>
/// Seconds the items should live in the cache
/// </summary>
public int Seconds { get; set; }
public bool NoDurationSet()
{
return Days == 0 && Hours == 0 && Minutes == 0 && Seconds == 0;
}
/// <summary>
/// The type of cache required for the item
/// </summary>
public CacheType CacheType { get; set; }
}