20 Mar

Output Cache Action Filter Attribute in ASP.NET Web API

Introduction

In this post we will learn how to implement output cache action filter attribute in ASP.NET Web API.

Implementation

First we will define the CacheEnabled and CacheTimespan properties in the appSettings.

  1. CacheEnabled  will be used enable or disable the cache
  2. CacheTimespan represents the time span that we will cache the output.

Next step we will create a class named WebApiOutputCacheAttribute.cs that defines action Filter to cache responses.

Add the following code snippet in WebApiOutputCacheAttribute class


/// <summary>
    /// Action Filter to cache responses
    /// </summary>
    public class WebApiOutputCacheAttribute : ActionFilterAttribute
    {
        // cache length in seconds
        private int _timespan;
        // true to enable cache
        private bool _cacheEnabled = false;
        // true if the cache depends on the caller user
        private readonly bool _dependsOnIdentity;
        // cache repository
        private static readonly ObjectCache WebApiCache = MemoryCache.Default;
        //private readonly SecurityHelper _securityHelper;
        private readonly bool _invalidateCache;

        /// <summary>
        /// Constructor
        /// </summary>
        public WebApiOutputCacheAttribute()
            : this(true)
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="dependsOnIdentity"></param>
        public WebApiOutputCacheAttribute(bool dependsOnIdentity)
            : this(dependsOnIdentity, false)
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="dependsOnIdentity"></param>
        /// <param name="invalidateCache">true to invalidate cache object</param>
        public WebApiOutputCacheAttribute(bool dependsOnIdentity, bool invalidateCache)
        {
            //_securityHelper = new SecurityHelper();
            _dependsOnIdentity = dependsOnIdentity;
            _invalidateCache = invalidateCache;

            ReadConfig();
        }

        /// <summary>
        /// Called by the ASP.NET MVC framework before the action method executes.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            if (_cacheEnabled)
            {
                if (filterContext != null)
                {
                    if (IsCacheable(filterContext))
                    {
                        string _cachekey = string.Join(":", new string[] 
                        { 
                            filterContext.Request.RequestUri.OriginalString,
                            filterContext.Request.Headers.Accept.FirstOrDefault().ToString(), 
                        });

                        //if (_dependsOnIdentity)
                        //    _cachekey = _cachekey.Insert(0, _securityHelper.GetUser());

                        if (WebApiCache.Contains(_cachekey))
                        {
                            //TraceManager.TraceInfo(String.Format("Cache contains key: {0}", _cachekey));

                            var val = (string)WebApiCache.Get(_cachekey);
                            if (val != null)
                            {
                                filterContext.Response = filterContext.Request.CreateResponse();
                                filterContext.Response.Content = new StringContent(val);
                                var contenttype = (MediaTypeHeaderValue)WebApiCache.Get(_cachekey + ":response-ct");
                                if (contenttype == null)
                                    contenttype = new MediaTypeHeaderValue(_cachekey.Split(':')[1]);
                                filterContext.Response.Content.Headers.ContentType = contenttype;
                                return;
                            }
                        }
                    }
                }
                else
                {
                    throw new ArgumentNullException("actionContext");
                }
            }
        }

        /// <summary>
        /// Called by the ASP.NET MVC framework after the action method executes.
        /// </summary>
        /// <param name="filterContext">The filter context.</param>
        public override void OnActionExecuted(HttpActionExecutedContext filterContext)
        {
            try
            {
                if (_cacheEnabled)
                {
                    if (WebApiCache != null)
                    {
                        string _cachekey = string.Join(":", new string[] 
                        { 
                            filterContext.Request.RequestUri.OriginalString,
                            filterContext.Request.Headers.Accept.FirstOrDefault().ToString(), 
                        });

                        //if (_dependsOnIdentity)
                        //    _cachekey = _cachekey.Insert(0, _securityHelper.GetUser());

                        if (filterContext.Response != null && filterContext.Response.Content != null)
                        {
                            string body = filterContext.Response.Content.ReadAsStringAsync().Result;

                            if (WebApiCache.Contains(_cachekey))
                            {
                                WebApiCache.Set(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
                                WebApiCache.Set(_cachekey + ":response-ct", filterContext.Response.Content.Headers.ContentType, DateTime.Now.AddSeconds(_timespan));
                            }
                            else
                            {
                                WebApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
                                WebApiCache.Add(_cachekey + ":response-ct", filterContext.Response.Content.Headers.ContentType, DateTime.Now.AddSeconds(_timespan));
                            }
                        }
                    }
                }

                if (_invalidateCache)
                {
                    CleanCache();
                }
            }
            catch (Exception ex)
            {
                //TraceManager.TraceError(ex);
            }
        }

        /// <summary>
        /// Removes all items from the cache
        /// </summary>
        private static void CleanCache()
        {
            if (WebApiCache != null)
            {
                List<string> keyList = WebApiCache.Select(w => w.Key).ToList();
                foreach (string key in keyList)
                {
                    WebApiCache.Remove(key);
                }
            }
        }

        private void ReadConfig()
        {
            if (!Boolean.TryParse(WebConfigurationManager.AppSettings["CacheEnabled"], out _cacheEnabled))
                _cacheEnabled = false;

            if (!Int32.TryParse(WebConfigurationManager.AppSettings["CacheTimespan"], out _timespan))
                _timespan = 1800; // seconds
        }

        private bool IsCacheable(HttpActionContext ac)
        {
            if (_timespan > 0)
            {
                if (ac.Request.Method == HttpMethod.Get)
                    return true;
            }
            else
            {
                throw new InvalidOperationException("Wrong Arguments");
            }
            return false;
        }
    }

Usage


public class UserDashboardController : ApiController
{
   private DBEntities db = new DBEntities();
   //If you want to disable cache pass parameter as false
   [WebApiOutputCacheAttribute(true)]
    public dynamic Get(long id)
    {
      return db.Users;
    }
}


Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>