I am following some practices documented by steven and using Simple Injector. I have a query that retrieves data from a WCF service and I want to cache the result using an instance of ObjectCache
.
I've defined a decorator CachingQueryHandlerDecorator<TQuery, TResult>
:
public sealed class CachingQueryHandlerDecorator<TQuery, TResult>
: IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
private readonly IQueryHandler<TQuery, TResult> _handler;
private readonly ObjectCache _cache;
private readonly CacheItemPolicy _policy;
private readonly ILog _log;
public CachingQueryHandlerDecorator(IQueryHandler<TQuery, TResult> handler,
ObjectCache cache,
CacheItemPolicy policy,
ILog log)
{
_handler = handler;
_cache = cache;
_policy = policy;
_log = log;
}
public TResult Handle(TQuery query)
{
var key = query.GetType().ToString();
var result = (TResult) _cache[key];
if (result == null)
{
_log.Debug(m => m("No cache entry for {0}", key));
result = (TResult)_handler.Handle(query);
if (!_cache.Contains(key))
_cache.Add(key, result, _policy);
}
return result;
}
}
Within SimpleInjectorInitializer.cs
I define the cache and policy, and add the decorator for a specific query:
container.RegisterSingle<ILog>(LogManager.GetCurrentClassLogger());
container.RegisterSingle<ObjectCache>(() => new MemoryCache("MyCache"));
container.RegisterSingle<CacheItemPolicy>(() => new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddMinutes(1) } );
.
.
.
container.RegisterDecorator(typeof(IQueryHandler<,>),
typeof(CachingQueryHandlerDecorator<,>),
ctx => ctx.ServiceType.GetGenericArguments()[0] == typeof(MyQuery));
The problem I'm facing is that I want to be able to specify different CacheItemPolicy
's for different queries. I could create a new ICachePolicy<TQuery>
interface and then define concrete classes for each different query type but I'm hoping there might be a way to avoid that and define the policy per query directly in the initialization file.
I could create a new ICachePolicy interface and then define concrete classes for each different query type
I think that's a pretty neat idea actually. You can register a default generic implementation that injected into every decorator that has no specific implementation registered:
container.RegisterOpenGeneric(typeof(ICachePolicy<>), typeof(DefaultCachePolicy<>),
Lifestyle.Singleton);
And for queries that have an alternative cache policy, you can register a specific implementation:
container.RegisterSingle<ICachePolicy<MyQuery>>(new CachePolicy<MyQuery>
{
AbsoluteExpiration = DateTime.Now.AddHour(2)
});
Another option is to mark queries or their query handlers with an attribute that describes the caching policy (this is the route I usually take):
[CachePolicy(AbsoluteExpirationInSeconds = 1 * 60 * 60)]
public class MyQuery : IQuery<string[]> { }
Now you don't have to inject an ICachePolicy<T>
, but can read this metadata directly using reflection:
public sealed class CachingQueryHandlerDecorator<TQuery, TResult>
: IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
private static readonly bool shouldCache;
private static readonly CachingPolicySettings policy;
private readonly IQueryHandler<TQuery, TResult> _handler;
private readonly ObjectCache _cache;
private readonly ILog _log;
static CachingQueryHandlerDecorator()
{
var attribute = typeof(TQuery).GetCustomAttribute<CachePolicyAttribute>();
if (attribute != null)
{
shouldCache = true;
policy = attribute.Policy;
}
}
public CachingQueryHandlerDecorator(
IQueryHandler<TQuery, TResult> handler,
ObjectCache cache,
ILog log)
{
_handler = handler;
_cache = cache;
_log = log;
}
public TResult Handle(TQuery query)
{
if (!shouldCache)
{
return this._handler.handle(query);
}
// do your caching stuff here.
}
You can achieve the result you require with an open generic implementation and override specific default values as required. I.e. you define an open generic implementation CachePolicy<TQuery>
of ICachePolicy<TQuery>
and use the RegisterInitializer
method to override parts of the default implementation.
Given these definitions:
public interface ICachePolicy<TQuery>
{
DateTime AbsoluteExpiration { get; }
}
public class CachePolicy<TQuery> : ICachePolicy<TQuery>
{
public CachePolicy()
{
AbsoluteExpiration = Cache.NoAbsoluteExpiration;
}
public DateTime AbsoluteExpiration { get; set; }
}
public interface IQueryHandler<TQuery, TResult> { }
public class QueryHandlerA : IQueryHandler<A, AResult> { }
public class QueryHandlerB : IQueryHandler<B, BResult> { }
public sealed class CachingQueryHandlerDecorator<TQuery, TResult>
: IQueryHandler<TQuery, TResult>
{
private readonly IQueryHandler<TQuery, TResult> decorated;
public readonly ICachePolicy<TQuery> Policy;
public CachingQueryHandlerDecorator(
IQueryHandler<TQuery, TResult> decorated,
ICachePolicy<TQuery> cachePolicy)
{
this.decorated = decorated;
this.Policy = cachePolicy;
}
}
Set up the container using the RegisterOpenGeneric
method and configure the non default values using RegisterInitializer
:
public Container ConfigureContainer()
{
Container container = new Container();
container.RegisterOpenGeneric(
typeof(ICachePolicy<>),
typeof(CachePolicy<>),
Lifestyle.Singleton);
container.RegisterInitializer<CachePolicy<A>>(a =>
a.AbsoluteExpiration = DateTime.Now.AddMinutes(1));
container.RegisterManyForOpenGeneric(
typeof(IQueryHandler<,>),
typeof(IQueryHandler<,>).Assembly);
container.RegisterDecorator(
typeof(IQueryHandler<,>),
typeof(CachingQueryHandlerDecorator<,>));
container.Verify();
return container;
}
These tests demonstrate the result is as expected:
[Test]
public void GetInstance_A_HasCustomAbsoluteExpiration()
{
Container container = ConfigureContainer();
var a = container.GetInstance<IQueryHandler<A, AResult>>();
Assert.AreNotEqual(
(a as CachingQueryHandlerDecorator<A, AResult>).Policy.AbsoluteExpiration,
Cache.NoAbsoluteExpiration);
}
[Test]
public void GetInstance_B_HasDefaultAbsoluteExpiration()
{
Container container = ConfigureContainer();
var b = container.GetInstance<IQueryHandler<B, BResult>>();
Assert.AreEqual(
(b as CachingQueryHandlerDecorator<B, BResult>).Policy.AbsoluteExpiration,
Cache.NoAbsoluteExpiration);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With