At the very first, “Happy New Year ” to all. In this article we will learn to implement Output Caching in Web API applications.
This article assumes familiarity with basic caching concepts and a good understanding of the Web API of MVC based applications. If you are very new to both concepts then my recommendation is to first learn those concepts then proceed with this article.
Most probably you have implemented caching in ASP.NET and the MVC Architecture. They are pretty easy. In the case of MVC just plug in the “[OutputCache]” attribute over action and it is done. And in the case of a form-based application we have implemented Output Caching by setting a proper page directive.
But when the scenario is Web API , the situation is a little more complex. Unfortunately the Web API doesn’t have built-in support to implement caching. But we can implement the caching functionality by implementing our own class.
First of all install the following package in your Web API application.
If you are running .NET 4.5 then go for this package.
- Install-Package Strathweb.CacheOutput.WebApi2
For .NET 4.5 , install the following one:
- Install-Package Strathweb.CacheOutput
Now we need to implement our own caching class that will be derived from the ActionFilterAttribute class. If you have worked with various filter actions then most probably you are already familiar with this class. So, let’s proceed step-by-step.
Step 1: Derive a class from the ActionFilterAttribute class
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
}
Step 2: Set the properties for the class:
private int _timespan;
// client cache length in seconds
private int _clientTimeSpan;
// cache for anonymous users only?
private bool _anonymousOnly;
// cache key
private string _cachekey;
// cache repository
private static readonly ObjectCache WebApiCache = MemoryCache.Default;
Step 3: Set the following constructor in the class:
public WebApiOutputCacheAttribute(int timespan, int clientTimeSpan, bool anonymousOnly)
{
_timespan = timespan;
_clientTimeSpan = clientTimeSpan;
_anonymousOnly = anonymousOnly;
}
Step 4: Check whether or not the request should be cached
In this step we will implement one private function that will determine whether or not the request should be cached.
private bool _isCacheable(HttpActionContext ac)
{
if (_timespan > 0 && _clientTimeSpan > 0)
{
if (_anonymousOnly)
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
return false;
if (ac.Request.Method == HttpMethod.Get) return true;
}
Else
{
throw new InvalidOperationException("Wrong Arguments");
}
return false;
}
Step 5: Function to enable client-side caching.
This is the function that will enable client-side caching:
private CacheControlHeaderValue setClientCache()
{
var cachecontrol = new CacheControlHeaderValue();
cachecontrol.MaxAge = TimeSpan.FromSeconds(_clientTimeSpan);
cachecontrol.MustRevalidate = true;
return cachecontrol;
}
Step 6: Override the onActionExecuting() function from the ActionAttributeFilter class.
public override void OnActionExecuting(HttpActionContext ac)
{
if (ac != null)
{
if (_isCacheable(ac))
{
_cachekey = string.Join(":", new string[] { ac.Request.RequestUri.AbsolutePath, ac.Request.Headers.Accept.FirstOrDefault().ToString() });
if (WebApiCache.Contains(_cachekey))
{
var val = (string)WebApiCache.Get(_cachekey);
if (val != null)
{
ac.Response = ac.Request.CreateResponse();
ac.Response.Content = new StringContent(val);
var contenttype = (MediaTypeHeaderValue)WebApiCache.Get(_cachekey + ":responsect");
if (contenttype == null)
contenttype = new MediaTypeHeaderValue(_cachekey.Split(':')[1]);
ac.Response.Content.Headers.ContentType = contenttype;
ac.Response.Headers.CacheControl = setClientCache();
return;
}
}
}
}
Else
{
throw new ArgumentNullException("actionContext");
}
}
Step 7: Override the OnActionExecuted function from the ActionAttributeFilter class.
public override void OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
if (!(WebApiCache.Contains(_cachekey)))
{
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
WebApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
WebApiCache.Add(_cachekey + ":response-ct",
actionExecutedContext.Response.Content.Headers.ContentType, DateTime.Now.AddSeconds(_timespan));
}
if (_isCacheable(actionExecutedContext.ActionContext))
actionExecutedContext.ActionContext.Response.Headers.CacheControl = setClientCache();
}
Step 8: The entire code of the WebApiOutputCacheAttribute class is given below for a better understanding.
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
private int _timespan;
// client cache length in seconds
private int _clientTimeSpan;
// cache for anonymous users only?
private bool _anonymousOnly;
// cache key
private string _cachekey;
// cache repository
private static readonly ObjectCache WebApiCache = MemoryCache.Default;
public WebApiOutputCacheAttribute(int timespan, int clientTimeSpan, bool anonymousOnly)
{
_timespan = timespan;
_clientTimeSpan = clientTimeSpan;
_anonymousOnly = anonymousOnly;
}
private bool _isCacheable(HttpActionContext ac)
{
if (_timespan > 0 && _clientTimeSpan > 0)
{
if (_anonymousOnly)
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
return false;
if (ac.Request.Method == HttpMethod.Get) return true;
}
else
{
throw new InvalidOperationException("Wrong Arguments");
}
return false;
}
private CacheControlHeaderValue setClientCache()
{
var cachecontrol = new CacheControlHeaderValue();
cachecontrol.MaxAge = TimeSpan.FromSeconds(_clientTimeSpan);
cachecontrol.MustRevalidate = true;
return cachecontrol;
}
public override void OnActionExecuting(HttpActionContext ac)
{
if (ac != null)
{
if (_isCacheable(ac))
{
_cachekey = string.Join(":", new string[] { ac.Request.RequestUri.AbsolutePath, ac.Request.Headers.Accept.FirstOrDefault().ToString() });
if (WebApiCache.Contains(_cachekey))
{
var val = (string)WebApiCache.Get(_cachekey);
if (val != null)
{
ac.Response = ac.Request.CreateResponse();
ac.Response.Content = new StringContent(val);
var contenttype = (MediaTypeHeaderValue)WebApiCache.Get(_cachekey + ":response
ct");
if (contenttype == null)
contenttype = new MediaTypeHeaderValue(_cachekey.Split(':')[1]);
ac.Response.Content.Headers.ContentType = contenttype;
ac.Response.Headers.CacheControl = setClientCache();
return;
}
}
}
}
Else
{
throw new ArgumentNullException("actionContext");
}
}
public override void OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext
actionExecutedContext)
{
if (!(WebApiCache.Contains(_cachekey)))
{
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
WebApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
WebApiCache.Add(_cachekey + ":response-ct",
actionExecutedContext.Response.Content.Headers.ContentType, DateTime.Now.AddSeconds(_timespan));
}
if (_isCacheable(actionExecutedContext.ActionContext))
actionExecutedContext.ActionContext.Response.Headers.CacheControl = setClientCache();
}
}
Step 9: Set action with attribute
We have finished the entire configuration, now we will set the attribute over the action. Here is the code of my Get() action within the Values controller.
[WebApiOutputCache(120, 60, false)]
public string Get()
{
return "Time is:-" + DateTime.Now.ToLongTimeString();
}
Now if we browse the Get() action then we will get the current time from the server. And the time will remain unchanged from any request within 2 minutes.
Step 10: Output