OutputCache is great for caching Actions with simple parameters, but when you are passing in a complex object it becomes a bit trickier to deal with.

After a little searching around the t'internet I found a few posts about creating a custom ActionFilterAttribute or inheriting from the OutputCacheAttribute and overriding the OnActionExecuting methods.

In the end, I went with a custom ActionFilterAttribute and although this isn't 'exactly' a full OutputCache implementation. It works in a similar way, and is really useful when you want to cache an ActionResult but when the data within the object changes the cache changes too.

Example of usage

[ChildActionOnly]
[OutputCacheComplex(typeof(BestSellersQuery), Duration = Constants.OutputCacheTime.SixHours)]
public ActionResult GetBestSellers(BestSellersQuery bestSellersQuery)
{
// Code here
}

I have a ChildAction that takes in a BestSellersQuery Object on different pages. The properties on the class range from Category ID's to DateTo and DateFrom to name a few.

The attribute below, uses Newtonsoft.Json to serialise the object(s) being passed to the ActionResult as part of a cache key.

The Attribute

/// <summary>
/// An attribute that allows output caching of an ActionResult based on the input being object(s)
/// </summary>
public class OutputCacheComplexAttribute : ActionFilterAttribute
{
    private readonly Type[] _types;

    private string _cachedKey;

    public int Duration { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="OutputCacheComplexAttribute"/> class.
    /// </summary>
    /// <param name="types">Types that this attribute will lookup for in QueryString/Form data and store values in cache.</param>
    /// <exception cref="System.ArgumentOutOfRangeException">type;type cannot be null</exception>
    public OutputCacheComplexAttribute(params Type[] types)
    {
        if (types == null)
        {
            throw new ArgumentOutOfRangeException("types", "type cannot be null");
        }
        _types = types;
        Duration = Constants.OutputCacheTime.TwelveHours;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var sbCachedKey = new StringBuilder();
        if (filterContext.HttpContext.Request.Url != null)
        {
            var path = filterContext.HttpContext.Request.Url.PathAndQuery;
            var parameters = filterContext.ActionParameters;
            foreach (var kv in parameters)
            {
                var kv1 = kv;
                if (kv.Value != null && _types.Any(t => t.IsInstanceOfType(kv1.Value)))
                {
                    var kvValue = JsonConvert.SerializeObject(kv.Value, Formatting.None, new JsonSerializerSettings
                    {
                        NullValueHandling = NullValueHandling.Ignore,
                        ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                    });
                    sbCachedKey = sbCachedKey.Append($"{kv.Key}:{kvValue};");
                }
            }

            _cachedKey = $"{GetType().Name}:{path}:{sbCachedKey}";
        }

        if (!string.IsNullOrWhiteSpace(_cachedKey) && filterContext.HttpContext.Cache[_cachedKey] != null)
        {
            filterContext.Result = (ActionResult)filterContext.HttpContext.Cache[_cachedKey];
        }
        else
        {
            base.OnActionExecuting(filterContext);
        }
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (!string.IsNullOrWhiteSpace(_cachedKey))
        {
            filterContext.HttpContext.Cache.Add(_cachedKey, filterContext.Result, null,
                DateTime.UtcNow.AddSeconds(Duration), Cache.NoSlidingExpiration,
                CacheItemPriority.Default, null);
        }

        base.OnActionExecuted(filterContext);
    }
}