Caching using decorator pattern in Episerver with StructureMap
I see many developers mix caching into their business logic. They usually start by making their own CachingService. Sounds like you are trying to reinvent the wheel.
I believe this is not nessesary and can easily be avoided with a different caching pattern.
Let me show you how we can implement this without having access to the actual implementation, without relying on a specific implementation and not changing any code using the service.
First let's set some requirements for the implementation:
- It must use the registrered implementation of the service to get the value.
- It must not change how you use the service in your code.
Let's dig in
In this example we have IService. It looks like this:
public interface IService
{
string SlowMethod(string key);
}
The implementation of the interface is a part of a nuget I have no access to.
Luckily this is an interface I can implement myself. So let's make one.
using system;
public class IServiceCacheDecorator : IService
{
public string SlowMethod(string key)
{
throw new NotImplementedException();
}
}
To fullfill the first requirement "It must use the actual implementation of IService to get the value.", we can use constructor injection to get the actual implementation.
public class IServiceCacheDecorator : IService
{
private readonly IService _service;
public IServiceCacheDecorator(IService service)
{
_service = service;
}
public string SlowMethod(string key)
{
return _service.SlowMethod(key);
}
}
In structuremap, this implementation can be used as a decorator.
So to use this decorator for IService we have to register it in StructureMap. Add the following code to your structuremap configuration class.
For<IService>().DecorateAllWith<IServiceCacheDecorator>();
A decorator is a way of wrapping code around an implementation of an interface.
Using DecorateAllWith is the same as doing this:
For<IService>().Use<IServiceCacheDecorator>()
.Ctor<IService>().Is<ServiceImplementation>();
So when getting an implementation of IService now we will get IServiceCacheDecorator which will call the registered implementation of IService.
Now I want to see if the value is cached before invoking SlowMethod, and return the cached value if it is. If it's not cached, add the value to the cache.
Let's add some caching.
using EPiServer.Framework.Cache;
public class IServiceCacheDecorator : IService
{
private readonly IService _service;
private readonly ISynchronizedObjectInstanceCache _cache;
private readonly TimeSpan _cacheTime = new TimeSpan(0, 60, 0);
private const string CacheStorageKey = nameof(IServiceCacheDecorator);
public IServiceCacheDecorator(IService service, ISynchronizedObjectInstanceCache cache)
{
_service = service;
_cache = cache;
}
public string SlowMethod(string key)
{
return _cache.ReadThrough(GetCacheKey(key), () => _service.SlowMethod(key), _ => GetPolicy(), ReadStrategy.Wait);
}
private string GetCacheKey(string key)
=> $"{nameof(IServiceCacheDecorator)}.{key}";
private CacheEvictionPolicy GetPolicy()
=> new CacheEvictionPolicy(_cacheTime, CacheTimeoutType.Absolute, Enumerable.Empty<string>(), new List<string>() { CacheStorageKey });
}
And we are done. This implementation will wait for the slow method to resolve if it's invoked multiple times, hindering a race condition.
Final thoughts
The decorator pattern is a clean way of adding extra logic without polluting your business logic or data access layer with caching code.
This implementation relies heavily on structuremap and Episerver's caching interfaces, but you can use MemoryCache or any other caching libraries or even a static dictionary if you really want to.
A last note: Sometimes, one decorator is not enough, e.g. when you are using a memory cache locally and a distributed cache, and you want to trigger some side effects when a specific method is invoked. This is possible with decorators. You can have multiple decorators on a single implementation of an interface. The decorators will be wrapping each other - every decorator wrapping the previous registered decorator.