8 Sep 2011

Using OutputCache with PartialView in MVC 3, with programmatic invalidation usingVaryByCustom too!

I've had a fun morning trying to cache partial views inside a dynamic page with MVC 3. Specifically, a partial view in a razor layout page (i.e. masterpage) that shows some dynamically generated data which I would like to hold off from regenerating for 60 seconds or so, even if the user refreshes or navigates around the site.

So I create my partial view like this - here we generate the time to show when the view is being cached

MyPartial.cshtml

Timestamp: @DateTime.Now.ToLongTimeString()

Then I create the controller action:

HomeController.cs

[OutputCache(Duration = 60)]
public ActionResult MyPartial()
{
    return PartialView();
}

Then I amend my layout page to include the call to the PartialView:

MyMaster.cshtml

Some header stuff
...
@Html.Action("MyPartial", "Home")
...
Some footer stuff

Well, that's all there is to it. The pitfalls I faced revolved around accidentally calling Html.Partial() from the layout page, which didn't work because it bypasses the controller attributes (i.e. it misses the OutputCache directive).

HOWEVER

Then I started wanting to reset the output cache when the underlying data changed, or when some user setting changed. So I knocked up this little solution using the OutputCache attribute's VaryByCustom property.

First a modification to the controller:

HomeController.cs

[OutputCache(Duration = 60, VaryByCustom = "WebApp.OutputCacheKey")]
public ActionResult MyPartial()
{
    return PartialView();
}

Every time OutputCache is called, it will check to see if that string has changed since last time. If it has changed, it invalidates the cache. "But it's a static string! How can it change?" I hear you cry. Yup, here's some voodoo magic. You can override a special method in global.asax that intercepts the call and returns ANOTHER string, wooo!

global.asax

public override string GetVaryByCustomString(HttpContext context, string arg)
{
    if (arg == "WebApp.OutputCacheKey")
    {
        return WebApp.OutputCacheKey;
    }
    else
    {
        return base.GetVaryByCustomString(context, arg);
    }
}

Now, we need to implement this WebApp thing. It's just an approach I like to have a static class knocking around my web application that allows me to access stuff from one central place, like strongly-typed accessors for session variables, or my per-request EF datacontext. But for these purposes it would just look like this:

WebApp.cs

public static class WebApp
{
    internal static string OutputCacheKey
    {
        get
        {
            if (HttpContext.Current.Session["WebApp.OutputCacheKey"] == null)
            {
                ResetOutputCache();
            }
            return HttpContext.Current.Session["WebApp.OutputCacheKey"].ToString();
        }
    }

    internal static void ResetOutputCache()
    {
        HttpContext.Current.Session["WebApp.OutputCacheKey"] = Guid.NewGuid().ToString();
    }    
}

So, what we have now is an easy way to programatically tell the OutputCache to clear itself, with a simple call from anywhere in your controller code:

WebApp.ResetOutputCache();

Simples!

7 comments:

  1. Anonymous4:58 pm

    Excellent!!!

    you are the man.
    I tried almost everything, and your solution is the only one that works and is simple.

    Power to the people!

    ReplyDelete
  2. Anonymous10:43 pm

    After searching on the internet for a simple way to cache my MVC 3 Razor application, I came across your blog and thought it would finally put an end to my coding frustrations. Unfortunately, I could not get your code to function properly within my application. Basically, I have an Index controller action that grabs data from a database but it’s very expensive at this time. So, I thought I would cache the controller action for 10 minutes but provide a link to the user to refresh the data at will. I wired the link up to the Refresh controller action then called your code snippet to reset the outputcache key but it’s not working. The initial call to the Index controller action works and caches the data in the browser. The next time I call the Index controller action it doesn’t utilize the cache but it pulls the data down from the server. As for the Refresh controller action, the first time I call the action controller via a link it doesn’t clear the cache but if I call the action a second time it clears the cache. Here’s my code:

    Static Class

    Public Class MyAppHelpers

    Friend Shared ReadOnly Property OutputCacheKey() As String
    Get
    If HttpContext.Current.Session("OutputCacheKey") Is Nothing Then
    ResetOutputCache()
    End If
    Return HttpContext.Current.Session("OutputCacheKey").ToString()
    End Get
    End Property

    Friend Shared Sub ResetOutputCache()
    HttpContext.Current.Session("OutputCacheKey") = Guid.NewGuid().ToString()
    End Sub

    End Class

    Global.asax.vb

    Public Overrides Function GetVaryByCustomString(context As HttpContext, arg As String) As String
    If arg = "OutputCacheKey" Then
    Return MyAppHelpers.OutputCacheKey
    Else
    Return MyBase.GetVaryByCustomString(context, arg)
    End If
    End Function

    View

    '
    ' GET: /Home

    Function Index(searchModel As SearchModel) As ActionResult

    End Function

    '
    ' GET: /Home/RefreshData
    Function RefreshData(searchModel As SearchModel) As ActionResult
    MyAppHelpers.ResetOutputCache()
    Return RedirectToAction("Index", New SearchModel With {.id = SearchModel.id})
    End Function

    ReplyDelete
  3. Anonymous10:45 pm

    Here's the correct Index controller action:

    '
    ' GET: /Home

    Function Index(searchModel As SearchModel) As ActionResult

    End Function

    ReplyDelete
  4. Anonymous10:46 pm

    “OutputCache(Location:=OutputCacheLocation.Client, Duration:=600, VaryByParam:="id", VaryByCustom:="OutputCacheKey")”

    ReplyDelete
  5. Anonymous7:45 am

    I am using similar approach for partial views. However, instead of using OutPutCache attribute with controller action, i wanted to use cache profile, that i can define in configuration file. But, while doing that, i face problem,

    "utputCacheAttribute for child actions only supports Duration, VaryByCustom, and VaryByParam values. Please do not set CacheProfile, Location, NoStore, SqlDependency, VaryByContentEncoding, or VaryByHeader values for child actions"

    ReplyDelete
  6. Thnz for sharing. Its very useful

    ReplyDelete
  7. It does not invalidate the cache, It only makes new one. So the resources it costs would be bigger and bigger every time you "clear the cache". And dependency on session? Huh, what a crazy technic.

    I would strongly not recommend this solution at all.

    ReplyDelete

Comments are very welcome unless you're a spammer, in which case you should probably kill yourself.

If I helped you out today, you can buy me a beer below. Cheers!