2

I have some controller actions that I want to have custom caching on. For example lets say I have a controller action ActionResult Index(string name) {}. I want to cache on the server the HTML of this action, unless there is a "live=true" querystring parameter in the url. If that parameter is present I would like to remove that action result from the server cache and serve the response normally.

We use OutputCache(Location=OutputCacheLocation.Server) attribute to do our caching usually. Is it possible to extend this attribute somehow and make it clear the cache if the live=true parameter is present in the URL?

Are there alternative that I can use to accomplish this if I can't customize the OutputCache attribute to get the behavior I need?

UPDATE

Based on James feedback here is the code that I have:

public class LiveOutputCacheAttribute : OutputCacheAttribute
{
    private const string _resetParam = "live";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var context = filterContext.HttpContext;
        AddLiveToVaryByParam();
        if (context.Request[_resetParam] == "true")
        {
            var urlToRemove = GetUrlToRemove(filterContext);
            context.Response.RemoveOutputCacheItem(urlToRemove);
            return;
        }
        base.OnActionExecuting(filterContext);
    }

    private void AddLiveToVaryByParam()
    {
        // add live reset flag when vary by param is specified
        if (VaryByParam != "*" && !VaryByParam.Contains("live"))
            VaryByParam = string.Format("{0};{1}",VaryByParam, _resetParam).TrimStart(';');
    }

    private static string GetUrlToRemove(ActionExecutingContext filterContext)
    {
        var routeValues = new RouteValueDictionary(filterContext.ActionParameters);
        var urlHelper = new UrlHelper(filterContext.RequestContext);
        string action = filterContext.ActionDescriptor.ActionName;
        string controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
        return urlHelper.Action(action, controller, routeValues);
    }
}

Here is how I use this on my action:

[LiveOutputCache(Location = OutputCacheLocation.Server, Duration = 60 * 60, VaryByParam = "name")]
public ActionResult Index(string name)
{
    ViewData.Model = name + "-----" +  DateTime.Now.Ticks.ToString();
    return View();
}

The problem is that when I use the live=true parameter, it is still not removing the original request from the cache. Am I doing something wrong here?

Vadim Rybak
  • 1,627
  • 19
  • 25
  • Maybe you are attempting to remove from the cache the entry that contains the extra parameter live=true, and not the one without it. Check the value of "urlToRemove" to verify it is correct before removing from the cache. – Pablo Romeo Aug 26 '12 at 20:26
  • I did look at the urlToRemove property and it does not have the "live" parameter there. Which is what I would expect because I am building that url from the Action parameters which do not have the live param. – Vadim Rybak Aug 27 '12 at 11:37

3 Answers3

3

You could use the VaryByParam attribute to check whether the live option is true e.g.

public class LiveOutputCacheAttribute : OutputCacheAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (VaryByParam == "true")
        {
            // clear cache
            return;   
        }

        base.OnActionExecuting(filterContext);
    }
}

...

[LiveOutputCache(Location=OutputCacheLocation.Server, VaryByParam="live")]
public ActionResult Index(string name) 
{
    ...
}

See How to programmatically clear outputcache for controller action method for the clearing part of it.

Community
  • 1
  • 1
James
  • 75,060
  • 17
  • 154
  • 220
0

You can't customize OutputCacheAttribute to get the behaviour, but you could write your own CustomCacheAttribute to achive this. To do this you can get sources of OutputCacheAttribute (MVC is opensource so you can do it), copy it and rewrite function OnActionExecuting(ActionExecutingContext filterContext).

Kirill Bestemyanov
  • 11,721
  • 2
  • 20
  • 37
0

Check out my blog post on creating your own Custom Output Cache in ASP.NET MVC http://bstavroulakis.com/blog/web/custom-output-caching-in-asp-net-mvc/

I had the following expectations from the output cache which weren't met

1) Have the ability to view the Caching Object when necessary and all of its children to invalidate parts when needed.

2) Have the ability to disable caching when needed.

3) Have some logic before and after the item was cached.

4) Have some parts dynamic on the site and load only those parts, having the rest of the site static

5) Use the cache structure in other parts of the site as well.

My actions where:

  • To create my own CacheManager that will add/remove/find/... objects in the cache.
public class CacheManager
    {
        #region ICacheManager Members

        public static void Add(string key, object value, int expireSeconds)
        {
            if (expireSeconds == CacheManagerKey.CacheLifeSpanForever)
                WebCache.Add(key, value, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
            else
                WebCache.Add(key, value, null, DateTime.MaxValue, TimeSpan.FromSeconds(expireSeconds), CacheItemPriority.Normal, null);
        }

        public static bool Contains(string key)
        {
            return WebCache.Get(key) != null;
        }

        public static int Count()
        {
            return WebCache.Count;
        }

        public static void Insert(string key, object value)
        {
            WebCache.Insert(key, value);
        }

        public static T Get(string key)
        {
            return (T)WebCache.Get(key);
        }

        public static List GetCacheKeys()
        {
            List keys = new List();
            foreach (DictionaryEntry entry in HttpContext.Current.Cache) keys.Add(entry.Key.ToString());
            return keys;
        }

        public static void Remove(string key)
        {
            WebCache.Remove(key);
        }

        public static void RemoveAll()
        {
            List keys = GetCacheKeys();
            foreach (string key in keys)
                WebCache.Remove(key);
        }

        public object this[string key]
        {
            get
            {
                return WebCache[key];
            }
            set
            {
                WebCache[key] = value;
            }
        }

        #endregion

        public static System.Web.Caching.Cache WebCache
        {
            get
            {
                System.Web.Caching.Cache cache = null;
                if (HttpContext.Current != null)
                    cache = HttpContext.Current.Cache;

                if (cache == null)
                    cache = HttpRuntime.Cache;

                return cache;
            }
        }
    }
    
  • After that I created my own attribute
        [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
        public class WebCacheAttribute : ActionFilterAttribute
        {
            public int Duration { get; set; }
            public string CacheKey { get; set; }
            public Dictionary CacheParams { get; set; }
            public Type CacheReturnType { get; set; }
            public string ContentType { get; set; }
            public HeaderContentTypeEnum ResponseHeaderContentType{get;set;}
            public string CacheObj { get; set; }
            private readonly ICacheHoleFiller _cacheHoleFiller;

            public WebCacheAttribute(int duration, string cacheKey, string cacheParamsStr, HeaderContentTypeEnum response = HeaderContentTypeEnum.Html, Type type = null)
            {

            }

            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {

            }

            public T GetCachedParam(Dictionary parameters, bool isAjaxRequest)
            {

            }

            public string GetUniqueKey(bool isAjaxRequest)
            {

            }

            public void OnException(ExceptionContext filterContext)
            {

            }

            private HtmlTextWriter tw;
            private StringWriter sw;
            private StringBuilder sb;
            private HttpWriter output;

            public override void OnResultExecuting(ResultExecutingContext filterContext)
            {

            }

            public override void OnResultExecuted(ResultExecutedContext filterContext)
            {

            }
    }
    
  • To have some parts dynamic I used "Donut Caching"

  • Lastly, I created a cachehelper to call other methods in my project that will use the webcache attribute as well.

var articleStr = CacheHelper.InvokeCacheMethod(typeof(HtmlHelperExtensions), "RenderArticlesCallback", new object[] { (int)articleType });
[WebCacheAttribute(CacheManagerKey.CacheLifeSpanForever, CacheManagerKey.Page_Article_Key, "articleTypeID")]
            public static string RenderArticlesCallback(int articleTypeID)
            {
public static class CacheHelper
    {
        public delegate object SourceDataDelegate(object[] args);

        public static T InvokeCacheMethod(Type type, string methodName, object[] args)
        {
            return (T)InvokeCacheMethod(type, methodName, null, args);
        }

        public static T InvokeCacheMethod(Type type, string methodName, object instance, object[] args)
        {
            var method = type.GetMethod(methodName);
            var webCache = method.ReturnParameter.Member.GetCustomAttributes(typeof(WebCacheAttribute), true).FirstOrDefault();
            Dictionary cacheParameters = FixCacheParameters(method, args);
            T cachedObj;

            if (Config.CacheEnabled && webCache != null)
            {
                cachedObj = ((WebCacheAttribute)webCache).GetCachedParam(cacheParameters, false);
                if (cachedObj != null)
                    return cachedObj;
            }
            T returnObj = (T)method.Invoke(instance, args);
            SaveCachedData(webCache, returnObj);
            return returnObj;
        }

        public static void SaveCachedData(object webCache, object returnObj)
        {
            if (Config.CacheEnabled && webCache != null)
            {
                var fullParamString = ((WebCacheAttribute)webCache).GetUniqueKey(false);
                CacheManager.Add(fullParamString, returnObj, ((WebCacheAttribute)webCache).Duration);
            }
        }

        public static Dictionary FixCacheParameters(MethodInfo method, object[] args)
        {
            Dictionary cacheParameters = new Dictionary();
            if (args != null)
            {
                var arguments = args.ToList();
                var count = 0;
                var argsCount = args.Length;
                var methodParameters = method.GetParameters().ToList();

                foreach (var argument in args)
                {
                    var key = methodParameters[count].Name;
                    object value = null;

                    if (argsCount > count)
                        value = args[count];

                    if (value != null && value.GetType() == typeof(string))
                        value = (object)value.ToString();

                    if (value != null)
                        cacheParameters.Add(key, value);

                    count++;
                }
            }

            return cacheParameters;
        }
    }

For more details on all of this you can visit my blog post => Custom Output Caching in ASP.NET MVC

billaraw
  • 938
  • 1
  • 7
  • 27