ASP.NET OutputCache and Cookies

asp.netcookiesoutputcache

Does anyone know why if is have cookies on my page, the output cache does not work !

Example page

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="ct.aspx.vb" Inherits="ct" %>
<%@ OutputCache Duration="600" Location="Server" VaryByParam="none" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <h1>Cache test</h1>
      <p id="rndout" runat="server"></p>
    </div>
    </form>
</body>
</html>

Example code behind:

Partial Class ct
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        Dim rc As New Random()
        Dim rn As Integer
        rn = rc.Next()
        rndout.InnerHtml = rn.ToString

        Response.Cookies("sym")("hello") = "world"
        Response.Cookies("sym").Expires = DateTime.Now.AddDays(370)
        Response.Cookies("sym").Domain = Application.Get("cookieurl")

    End Sub
End Class

when deployed to iis 6 or 7 this does not cache, however if i comment out the 3 Response.Cookies lines it does.

When run up in VS it works fine both ways.

Is there some setting in iis/web.config etc to allow outputcache while i set response.cookies. I understand the cookie content will be cached as well as it is just a part of the http response that is cached.

Best Answer

After doing a fair bit of research into this problem, I came to understand and work-around the problem.

The reason output cache doesn't play nice with cookies

So the reason the output cache will not cache a response with cookies is that a cookie could be user-specific (e.g. authentication, analytical tracking, etc.). If one or more cookies with the property HttpCookie.Shareable = false, then the output cache considers the response uncacheable.

Including cookies with a cached response

This is where it get's tricky. The output cache caches the response headers and content together and doesn't provide any hooks to modify these before sending them back to the user. However, I wrote the following custom output cache provider to provide the ability to modify a cached response's headers before they are sent back to the user (requires the Fasterflect nuget package):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Web;
using System.Web.Caching;
using Fasterflect;

namespace CustomOutputCache
{
    /// <summary>
    /// An output cache provider that has ability to modify the http header collection before a cached response is served back to the user.
    /// </summary>
    public class HeaderModOutputCacheProvider : OutputCacheProvider
    {
        private static readonly Type OutputCacheEntryType, HttpCachePolicySettingsType;
        private static readonly Type[] ParameterTypes;

        public static event EventHandler<CachedRequestEventArgs> RequestServedFromCache;

        static HeaderModOutputCacheProvider()
        {
            var systemWeb = typeof(HttpContext).Assembly;
            OutputCacheEntryType = systemWeb.GetType("System.Web.Caching.OutputCacheEntry");
            HttpCachePolicySettingsType = systemWeb.GetType("System.Web.HttpCachePolicySettings");
            ParameterTypes = new[]{
                typeof(Guid),
                HttpCachePolicySettingsType,
                typeof(string),
                typeof(string) ,
                typeof(string[]),
                typeof(int),
                typeof(string),
                typeof(List<HeaderElement>),
                typeof(List<ResponseElement>)
            };
        }

        private readonly ObjectCache _objectCache;

        public HeaderModOutputCacheProvider()
        {
            _objectCache = new MemoryCache("output-cache");
        }

        #region OutputCacheProvider implementation

        public override object Get(string key)
        {
            var cachedValue = _objectCache.Get(key);

            if (cachedValue == null)
                return null;

            if (cachedValue.GetType() != OutputCacheEntryType)
                return cachedValue;

            var cloned = CloneOutputCacheEntry(cachedValue);

            if (RequestServedFromCache != null)
            {
                var args = new CachedRequestEventArgs(cloned.HeaderElements);
                RequestServedFromCache(this, args);
            }

            return cloned;
        }

        public override object Add(string key, object entry, DateTime utcExpiry)
        {
            _objectCache.Set(key, entry, new CacheItemPolicy { AbsoluteExpiration = utcExpiry });
            return entry;
        }

        public override void Set(string key, object entry, DateTime utcExpiry)
        {
            _objectCache.Set(key, entry, new CacheItemPolicy { AbsoluteExpiration = utcExpiry });
        }

        public override void Remove(string key)
        {
            _objectCache.Remove(key);
        }

        #endregion

        private IOutputCacheEntry CloneOutputCacheEntry(object toClone)
        {
            var parameterValues = new[]
            {
                toClone.GetFieldValue("_cachedVaryId", Flags.InstancePrivate),
                toClone.GetFieldValue("_settings", Flags.InstancePrivate),
                toClone.GetFieldValue("_kernelCacheUrl", Flags.InstancePrivate),
                toClone.GetFieldValue("_dependenciesKey", Flags.InstancePrivate),
                toClone.GetFieldValue("_dependencies", Flags.InstancePrivate),
                toClone.GetFieldValue("_statusCode", Flags.InstancePrivate),
                toClone.GetFieldValue("_statusDescription", Flags.InstancePrivate),
                CloneHeaders((List<HeaderElement>)toClone.GetFieldValue("_headerElements", Flags.InstancePrivate)),
                toClone.GetFieldValue("_responseElements", Flags.InstancePrivate)
            };

            return (IOutputCacheEntry)OutputCacheEntryType.CreateInstance(
                parameterTypes: ParameterTypes,
                parameters: parameterValues
            );
        }

        private List<HeaderElement> CloneHeaders(List<HeaderElement> toClone)
        {
            return new List<HeaderElement>(toClone);
        }
    }

    public class CachedRequestEventArgs : EventArgs
    {
        public CachedRequestEventArgs(List<HeaderElement> headers)
        {
            Headers = headers;
        }
        public List<HeaderElement> Headers { get; private set; }

        public void AddCookies(HttpCookieCollection cookies)
        {
            foreach (var cookie in cookies.AllKeys.Select(c => cookies[c]))
            {
                //more reflection unpleasantness :(
                var header = cookie.CallMethod("GetSetCookieHeader", Flags.InstanceAnyVisibility, HttpContext.Current);
                Headers.Add(new HeaderElement((string)header.GetPropertyValue("Name"), (string)header.GetPropertyValue("Value")));
            }
        }
    }
}

You would wire it up like this:

<system.web>
  <caching>
      <outputCache defaultProvider="HeaderModOutputCacheProvider">
        <providers>
          <add name="HeaderModOutputCacheProvider" type="CustomOutputCache.HeaderModOutputCacheProvider"/>
        </providers>
      </outputCache>
    </caching>
  </system.web>

And could use it like this to insert cookies:

HeaderModOutputCacheProvider.RequestServedFromCache += RequestServedFromCache;

HeaderModOutputCacheProvider.RequestServedFromCache += (sender, e) =>
{
    e.AddCookies(new HttpCookieCollection
    {
        new HttpCookie("key", "value")
    });
};