ASP.NET OutputCache and Cookies
Asked Answered
B

6

9

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.

Bacteria answered 23/2, 2012 at 10:28 Comment(3)
I found the same thing to be true but haven't encountered any official docs. explicitly stating that it doesn't work.Neves
@Allov, sorry for the delay - no i have not got a solution. Except to get rid of the cookie or if i need a cookie i can add a script tag or 0x0 image on the page that just sets the cookie.Bacteria
A couple years too late, but this guy found a workaround somehow. No word yet how risky it is. https://mcmap.net/q/1175954/-net-outputcache-directive-not-workingRelationship
C
6

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")
    });
};
Confiture answered 31/3, 2016 at 9:12 Comment(0)
C
4

You try to cache this on server side, and at the same time you try to set the cookie on the client - this is not working together.

Why: When you set a page on cache on server side the code behind is not run when the cached version is served (send to client). This is the point of caching on server. To not run anything and give it from the cache as it is.

Maybe you need to just set the cache on header and not cache the full page on server.

Cacology answered 23/2, 2012 at 10:50 Comment(3)
I am creating a page in asp.net that has a cookie. I want iis to cache this page and not run the code behind. I am using the standard .net code to do this. However it looks like if i use response.cookie in any way the outputcache directive is broke. This is not documented in any way within .net. in fact there is an article saying remember if you cache a page with cookies the cookies are cached as well. I am well aware that the cookie is part of the http headers and so will be cached. my question is if there is a setting in iis/web.config etc that enables this. It works ok when run in cassiniBacteria
@Symeon this is not logical (buggy) what you try to do. You set a cookie to one user, what about the next user that have no cookie set ? The cookie set on client - you have mix up the client cache with the server cache. The cookie are also hold on cache when they are on client side not on serverCacology
Whoever visits the page will be given a cookie, whether it exist or not. A cookie is just text in a http header. I can see why it may be confusing, but i just thought there must have been some configuration as no where does it state they are mutually exclusive, and it works ok in cassini. look at this -support.microsoft.com/kb/917072 sounds like it SHOULD be caching with the cookie as they give a workaround to stop it.Bacteria
S
3

It's caused by different versions of .NET framework. Basically, some versions will never cache page with cookie set.

See this blog posting.

Samuel answered 5/10, 2012 at 15:47 Comment(1)
Welcome to Stack Overflow! Thanks for posting your answer! Please be sure to read the FAQ on Self-Promotion carefully. Also note that it is required that you post a disclaimer every time you link to your own site/product.Invite
B
1

Check to see if you're running .NET 2.0 SP1 and if you've applied MS11-100 (released December 2012).

We faced similar issues and ended up reaching out to Microsoft Support. They confirmed MS11-100 breaks output caching but claimed it was by design (due to the nature of the security vulnerabilities fixed in the patch) and there is currently nothing being done to restore output cache functionality.

A simple test: if you find you have the patch installed, simply uninstall that patch and reboot. You should see that output caching begins to work. I don't think anyone would recommend this as a production solution due to the security implications so only use this as a means to isolate the problem.

We ended up testing a newer framework (you have to go to 4.0; 3.5 is just an extension of the 2.0 framework and not a standalone framework on its own) and, after resolving all compilation errors, output caching began working immediately.

We also worked on changing the way we interact with cookies so that we could stay on the 2.0 framework (after all, it should be easier to test our cookie handler classes instead of testing the whole entire application). There are a number of hurdles and the final product reeked of "hacks" so that was a no-go.

Blah answered 8/6, 2012 at 14:52 Comment(0)
S
0

I was having the same problem and I tested the scenario given by Aristos by setting Location="ServerAndClient" and it works. If I only use Location="Server" then it didn't worked.

Smithery answered 30/5, 2012 at 17:58 Comment(1)
In your case, only client (HTTP response header caching) will work. The page output won't be cached on the server if you set cookies in your response.Salish
D
-1

There is a workaround which might work in some scenarios: If the cookie does not depend heavily on the page code but can be calculated with some standalone code, you can set the cookie in the Application_EndRequest The Application_EndRequest is processed after the OutputCache and therefore the cache is stored with no cookie but then the set cookie header is added before the request is delivered to the client.

Disperse answered 27/11, 2012 at 20:29 Comment(1)
I tried this method and received a "cannot modify headers after response has been sent error".Confiture

© 2022 - 2024 — McMap. All rights reserved.