Customizable and extendable Web API caching with CacheManager, Strathweb.CacheOutput and Redis in the backend.
Source code for solution is available here https://github.com/mchudinov/WebApiCache
I like Strathweb.CacheOutput library. It gives cache attribute to Web API actions in a similar way as it is in ASP.NET MVC. To make cache flexible we use CacheManager. It is easy to change cache backed with CacheManager by just changing config options.
1. Install packages
- Newtonsoft.Json
- Microsoft.Web.RedisSessionStateProvider
- StackExchange.Redis.StrongName
- CacheManager.Core
- CacheManager.Serialization.Json
- CacheManager.StackExchange.Redis
- Strathweb.CacheOutput.WebApi2
2. Simple cache with CacheManager and Redis
The simple cache solution based on CacheManager and Redis is described here:
ASP.Net cache and session with Redis and CacheManager
and here is my blogpost about common cache interface and implementations: Common cache interface and implementations
3. Add implementation of WebApi.OutputCache.Core.Cache.IApiOutputCache
OuputCache has it’s own cache interface that must be implemented in order to customize backend.
public interface IApiOutputCache { IEnumerable<string> AllKeys { get; } void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null); bool Contains(string key); T Get<T>(string key) where T : class; void Remove(string key); void RemoveStartsWith(string key); }
I use implementation based on my CacheManager class. Read more about it in my blog post here ASP.Net cache and session with Redis and CacheManager
class CustomCacheProvider : IApiOutputCache { private static readonly ICacheProvider CacheManagerCache = new CacheManagerCache(); private static readonly IList<string> Keys = new List<string>(); public void RemoveStartsWith(string key) { IList<string> keys = Keys.Where(k => k.StartsWith(key)).ToList(); foreach (var k in keys) { Remove(k); } } public T Get<T>(string key) where T : class { return CacheManagerCache.Get<T>(key); } public object Get(string key) { return CacheManagerCache.Get<object>(key); } public void Remove(string key) { Keys.Remove(key); CacheManagerCache.Remove(key); } public bool Contains(string key) { return CacheManagerCache.Exists(key); } public void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null) { Keys.Add(key); CacheManagerCache.Set(key, o, expiration); } public IEnumerable<string> AllKeys => Keys; }
Add call in Configuration method in startup class:
config.CacheOutputConfiguration().RegisterCacheOutputProvider(() => new CustomCacheProvider());
4. Add custom key generator if needed
public class CustomCacheKeyGenerator : WebApi.OutputCache.V2.DefaultCacheKeyGenerator { public override string MakeCacheKey(HttpActionContext actionContext, MediaTypeHeaderValue header, bool excludeQueryString = false) { //add your key generation here return base.MakeCacheKey(actionContext, header, excludeQueryString); } }
Add call in Configuration method in Startup class:
config.CacheOutputConfiguration().RegisterDefaultCacheKeyGeneratorProvider(() => new CustomCacheKeyGenerator());
Test the cache invalidation actually works with new key generation routine! Run some POST or PUT calls and watch that keys are deleted from cache. If they are not, then the custom generated keys are not good.
5. Configure Redis in .config file
<cacheManager.Redis> <connections> <connection id="RedisHandle" database="0" connectionString=".....:6380,password=....,ssl=True,abortConnect=False" /> </connections> </cacheManager.Redis> <cacheManager> <managers> <cache name="Redis" updateMode="Up" enableStatistics="false" enablePerformanceCounters="false" serializerType="CacheManager.Serialization.Json.JsonCacheSerializer, CacheManager.Serialization.Json"> <handle name="RedisHandle" ref="RedisHandle" /> </cache> </managers> <cacheHandles> <handleDef id="RedisHandle" type="CacheManager.Redis.RedisCacheHandle`1, CacheManager.StackExchange.Redis" /> </cacheHandles> </cacheManager>
6. Create WebAPI action with cache attributes and test cache
Add cache attributes. Cache must be used on GET methods, and invalidated (keys deleted) on POST,PUT and DELETE methods.
[AutoInvalidateCacheOutput] [CacheOutput(ServerTimeSpan = 60)] public class CacheController : ApiController { // GET api/cache public IEnumerable<string> Get() { Console.WriteLine("return from method"); return new[] { "value1", "value2" }; } public void Post(object obj) { // } }
Now we can change cache backend for WebAPi by simply changing configuration.
Solution runs selfhosted WebAPI on localhost::8080 and a WebApiClient from a second console window.