views:

85

answers:

2

I have a menu on my site that changes depending on whether the user is logged in or not. With browser caching, the menu "gets stuck" in either state and is confusing to users.

They'll login, but the menu won't update because it's still cached in the unauthenticated state... and vice versa.

How is this typically handled? Can we refresh the user's browser cache from our code? Or do I just not allow browser caching? (would rather use it, very nice bump in speed).

Update

Here's how I set client-side, browser caching in my asp.net mvc 2 app:

public class CacheFilterAttribute : ActionFilterAttribute {
    /// <summary>
    /// Gets or sets the cache duration in seconds. The default is 10 seconds.
    /// </summary>
    /// <value>The cache duration in seconds.</value>
    public int Duration { get; set; }

    public CacheFilterAttribute() { Duration = 10; }

    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        if (Duration <= 0) return;

        var cache = filterContext.HttpContext.Response.Cache;
        var cacheDuration = TimeSpan.FromSeconds(Duration);

        cache.SetCacheability(HttpCacheability.Public);
        cache.SetExpires(DateTime.Now.Add(cacheDuration));
        cache.SetMaxAge(cacheDuration);
        cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
    }
}

And then apply a [CachFilter(Duration = 60)] to my actions. (note, I got the code above from Kazi Manzur Rashid's Blog

+1  A: 

You might want to try donut hole caching and cache everything except the menu. Scott Guthrie has a nice article on the topic.

tvanfosson
Thanks for the link. I'm aware of output caching in asp.net. What I was referring to is the client-side, browser caching. I edited my answer to show how I set browser cache timeout. Thanks!
Chad
A: 

I've been playing around a bit with an action filter to simulate that behavior using conditional requests and e-tags, it's pure client side caching so no output caching involved here. You still get a request to the server but the action will not be called and the client will use the local cache if it's still fresh.

/// <summary>
/// Handles client side caching by automatically refreshing content when switching logged in identity
/// </summary>
public class ClientCacheByIdentityAttribute : ActionFilterAttribute
{
    /// <summary>
    /// Sets the cache duraction in minutes
    /// </summary>
    public int Duration { get; set; }

    /// <summary>
    /// Check for incoming conditional requests
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.IsChildAction || filterContext.HttpContext.Request.RequestType!="GET" || filterContext.Result!=null)
        {
            return;
        }

        string modifiedSinceString = filterContext.HttpContext.Request.Headers["If-Modified-Since"];
        string noneMatchString = filterContext.HttpContext.Request.Headers["If-None-Match"];

        if (String.IsNullOrEmpty(modifiedSinceString) || String.IsNullOrEmpty(noneMatchString))
        {
            return;
        }

        DateTime modifiedSince;

        if (!DateTime.TryParse(modifiedSinceString, out modifiedSince))
        {
            return;
        }

        if (modifiedSince.AddMinutes(Duration) < DateTime.Now)
        {
            return;
        }

        string etag = CreateETag(filterContext.HttpContext);

        if (etag == noneMatchString)
        {
            filterContext.HttpContext.Response.StatusCode = 304;
            filterContext.Result = new EmptyResult();
        }
    }

    /// <summary>
    /// Handles setting the caching attributes required for conditional gets
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.HttpContext.Request.RequestType == "GET" && filterContext.HttpContext.Response.StatusCode == 200 && !filterContext.IsChildAction && !filterContext.HttpContext.Response.IsRequestBeingRedirected)
        {
            filterContext.HttpContext.Response.AddHeader("Last-Modified", DateTime.Now.ToString("r"));
            filterContext.HttpContext.Response.AddHeader("ETag", CreateETag(filterContext.HttpContext));
        }
    }

    /// <summary>
    /// Construct the ETag
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    private static string CreateETag(HttpContextBase context)
    {
        return "\"" + CalculateMD5Hash(context.Request.Url.PathAndQuery + "$" + context.User.Identity.Name) + "\"";
    }

    /// <summary>
    /// Helper to make an MD5 hash
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    private static string CalculateMD5Hash(string input)
    {
        MD5 md5 = MD5.Create();
        byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
        byte[] hash = md5.ComputeHash(inputBytes);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < hash.Length; i++)
        {
            sb.Append(hash[i].ToString("X2"));
        }
        return sb.ToString();
    }
}
Per Bjurström
Thanks, I'll check this out as soon as I can. I'll report back what if it helps me out. Much appreciated.
Chad