This article explores the different possibilities for caching Web API services. At present Microsoft do not support any decent cache solution for Web API. This might change with vNext when MVC and Web API come together as MVC already has an Output Cache for controllers.
Code: https://github.com/damienbod/WebAppCacheCow
At present, I know of 3 different ways to implement cache for Web API. All have advantages and disadvantages.
Possible Web API caching solutions:
- CacheCow
- AspNetWebApi-OutputCache
- Do it yourself; ETAGs memory, etc
Example using CacheCow.
CacheCow from Ali Kheyrollahi is very easy to use and can be easily added or removed from your project. It does not require attributes on all your controllers. To use, just download the NuGet package:
And add this to your web API config:
// In memory var cacheCowCacheHandler = new CachingHandler(config); config.MessageHandlers.Add(cacheCowCacheHandler);
Now you have an in-memory caching.
If you require more information on how this works, or info about ETAGS, refer to this post:
http://bitoftech.net/2014/02/08/asp-net-web-api-resource-caching-etag-cachecow/
Or direct: http://byterot.blogspot.ch/
It is also simple to use persistent cache. CacheCow has many persistent types supported and it is easy to implement your own.
Here’s an example of an Elasticsearch Store.
CacheCow supplies an IEntityTagStore interface which can be implemented for any specific store:
using System; using System.Collections.Generic; using System.Linq; using CacheCow.Common; using Nest; namespace CacheCow.Server.EntityTagStore.Elasticsearch { public class NestEntityTagStore : IEntityTagStore { private readonly ElasticClient _elasticsearchClient; private const string ElasticsearchIndex = "cachecow"; public NestEntityTagStore(string elasticsearchUrl) { var uri = new Uri(elasticsearchUrl); var settings = new ConnectionSettings(uri).SetDefaultIndex(ElasticsearchIndex); _elasticsearchClient = new ElasticClient(settings); } private PersistentCacheKey TryGetPersistentCacheKey(string key) { var idsList = new List<string> { key }; var result = _elasticsearchClient.Search<PersistentCacheKey>(s => s .Index(ElasticsearchIndex) .AllTypes() .Query(p => p.Ids(idsList))); if (result.Documents.Any()) { return result.Documents.First(); } return null; } public bool TryGetValue(CacheKey key, out TimedEntityTagHeaderValue eTag) { eTag = null; var persistentCacheKey = TryGetPersistentCacheKey(key.HashBase64); if (persistentCacheKey != null) { eTag = new TimedEntityTagHeaderValue(persistentCacheKey.ETag) { LastModified = persistentCacheKey.LastModified }; return true; } return false; } public void AddOrUpdate(CacheKey key, TimedEntityTagHeaderValue eTag) { var cacheKey = TryGetPersistentCacheKey(key.HashBase64); if (cacheKey != null) { // update existing cacheKey.ETag = eTag.Tag; cacheKey.LastModified = eTag.LastModified; } else { // Create new cacheKey = new PersistentCacheKey { Id = key.HashBase64, RoutePattern = key.RoutePattern, ETag = eTag.Tag, LastModified = eTag.LastModified, ResourceUri = key.ResourceUri }; } _elasticsearchClient.Index(cacheKey, PersistentCacheKey.SearchIndex, ElasticsearchIndex); } public int RemoveResource(string resourceUri) { var items = resourceUri.Trim('/').Split('/'); var result = _elasticsearchClient.Search<PersistentCacheKey>(s => s.Index(ElasticsearchIndex) .AllTypes() .Query(q => q.TermsDescriptor(tq => tq .OnField(f => f.ResourceUri) .Terms(items) .MinimumMatch(items.Length) ) )); int count = result.Documents.Count(); foreach (var item in result.Documents) { _elasticsearchClient.DeleteById(ElasticsearchIndex, ElasticsearchIndex, item.Id); } return count; } public bool TryRemove(CacheKey key) { var idsList = new List<string> { key.HashBase64 }; var result = _elasticsearchClient.Search<PersistentCacheKey>(s => s .Index(ElasticsearchIndex) .AllTypes() .Query(p => p.Ids(idsList))); int count = result.Documents.Count(); foreach (var item in result.Documents) { _elasticsearchClient.DeleteById(ElasticsearchIndex, ElasticsearchIndex, item.Id); } return count > 0; } public int RemoveAllByRoutePattern(string routePattern) { var items = routePattern.Trim('+').Trim('/').Split('/'); var result = _elasticsearchClient.Search<PersistentCacheKey>(s => s.Index(ElasticsearchIndex) .AllTypes() .Query(q => q.TermsDescriptor(tq => tq .OnField(f => f.ResourceUri) .Terms(items) .MinimumMatch(items.Length) ) )); int count = result.Documents.Count(); foreach (var item in result.Documents) { _elasticsearchClient.DeleteById(ElasticsearchIndex, ElasticsearchIndex, item.Id); } return count; } public void Clear() { _elasticsearchClient.DeleteIndex(ElasticsearchIndex); } public void Dispose() { } } }
The store requires an entity class which is persisted in Elasticsearch:
using System; namespace CacheCow.Server.EntityTagStore.Elasticsearch { public class PersistentCacheKey { public const string SearchIndex = "cachecow"; public string Id { get; set; } public string RoutePattern { get; set; } public string ResourceUri { get; set; } public string ETag { get; set; } public DateTimeOffset LastModified { get; set; } } }
This can then be added to your Web API config:
//Configure HTTP Caching using Elasticsearch IEntityTagStore eTagStore = new CacheCow.Server.EntityTagStore.Elasticsearch.NestEntityTagStore("http://localhost:9200"); var cacheCowCacheHandler = new CachingHandler(config, eTagStore) { AddLastModifiedHeader = false }; config.MessageHandlers.Add(cacheCowCacheHandler);
Here’s an example of how it looks in the store:
Example of Caching using AspNetWebApi-OutputCache from Filip WOJCIESZYN
This cache solution works as described on github. It is also very easy to be use and is easy to extend. It is more tightly coupled to the application than CacheCow. (Attributes are used in the controller. ) To extend it, the IApiOutputCache interface needs to be implemented.
You can download this package:
And then the caching can be used:
// GET api/values/5 [CacheOutput(ServerTimeSpan = 500, ExcludeQueryStringFromCacheKey = true)] public string Get(int id) { return "value"; }
Only one implementation of persistent cache exists for this, an implementation by Alex James Brown:
https://github.com/alexjamesbrown/AspNetWebApi-OutputCache-MongoDb
The library itself provides no persistence cache solutions unlike CacheCow.
Summany
It is hard to compare the 2 solutions as both work different and depending on your requirements one might have an slight advantage. All I can say is thanks to the authors for providing the solutions.
Links:
http://byterot.blogspot.ch/2012/09/asp-net-web-api-consumption-cachecow-client.html
http://byterot.blogspot.co.uk/2012/07/introducing-cachecow-http-caching.html
https://github.com/aliostad/CacheCow
http://www.asp.net/web-api/overview/formats-and-model-binding/content-negotiation
https://github.com/filipw/AspNetWebApi-OutputCache
http://piusnjoka.com/2013/05/10/implementing-entity-tag-in-asp-net-web-api/
http://bitoftech.net/2014/02/08/asp-net-web-api-resource-caching-etag-cachecow/
http://weaklinglifter.blogspot.ch/2014/02/webapi101-http-caching-on-action.html
The TryGetValue method doens’t seem to be returning any content. How does CacheCow know what value to use based on just the eTag and the LastModified values?