Exploring Web API 2 Caching

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:

  1. CacheCow
  2. AspNetWebApi-OutputCache
  3. 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:

cacheCowExample_02

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:
cacheCowExample_01

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:
cacheExample_03

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://codebetter.com/howarddierking/2011/07/01/automatic-etag-management-with-web-api-message-handlers/

http://piusnjoka.com/2013/05/10/implementing-entity-tag-in-asp-net-web-api/

http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx

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

http://nest.azurewebsites.net/

One comment

  1. 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?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: