RSS

Overriding Browser Caching with ASP.NET MVC

05 Mar

It’s a well-known issue that most browsers by default cache static content such as CSS or JavaScript files. You can make changes to these files on the server, and force your own browser to refresh it’s cache in order to view the latest version of these files, but you can’t force other people’s browsers to perform cache invalidation. True, you can use ETags in order to control this to a certain extent, but I found this to be non-trivial as the altervative. As a result, clients may experience what would seem like a broken html/css (or JavaScript) experience, which is bad.

One known trick is to “timestamp” your css/js references. Simply done, this means that you add some sort of version (or timestamp) to your css/js reference as a query string item. While meaningless to the css/js file itself, the browser caches according to the complete url, with query string, so providing a url which is new to the browser will cause it to download the updated file from the server.

For example:

<script type='text/javascript' src='http://.../myScripts.js?v=1'></script> 

While this was an easy solution, I still had to remember to increment the “version” specified in the query string, each time we updated the file. In development, this is quite often and therefore proved to be quite a “nagging” experience.

In ASP.NET WebForms, I used to handle this problem automatically when it came down to JavaScript files. In the different projects we used ScriptManager (and ScriptManagerProxy) often, so all JavaScripts were referenced using asp:ScriptReference. This was originally intended to solve JavaScript relativity issues with the files locations. You could simply specify a tilde (~), and the file location was resolved automatically by ASP.NET. Consider this code for example:

1: <asp:ScriptManager  runat="server"> 
2:     <Scripts> 
3:         <asp:ScriptReference  Path="~/Scripts/jquery-1.4.1.min.js"  /> 
4:     </Scripts> 
5: </asp:ScriptManager>

Using ScriptReference also provided a solution to “timestamping” the file. Using a recursion, I iterated over all ScriptManager and ScriptManagerProxy controls, usually later in the lifecycle, and added a timestamp to the ScriptReference. The only trick required was to add a version automatically so that the browser will actually load the latest version from the server, without losing the browser caching itself for files not updated. In other words, we only need to to “override” the browser cache when a file was actually updated. If not updated, we’d still like to enjoy browser caching. So the recursion mentioned earlier, which timestamped the ScriptReferences, did exactly that: we added to the querystring the actual modification date/time of the file. As long as the file was not updated, the browser used the cached version of the file. Once updated, the rendered Html contained a file reference with a newer timestamp.

While this solved the problem for JavaScript files, it didn’t provide a solution to CSS files in App_Themes, as we had no control over these (actually, it’s possible that these can be controlled too…)

Funnily enough, one of the advantages of ASP.NET MVC in this area is that there is no ScriptManager. This meant that I had to search for an alternative way to automatically timestamping the file references. Although ASP.NET MVC “lacks” WebForm server side controls, it is very convenient and flexible in the markup View. So I came up with a simple piece of code which solved the timestamping and the file relativity problem all together. No longer do I write html <script> tags when I reference a JavaScript file. Instead, I use the following extension method:

@Url.Script("~/Scripts/jquery-1.4.4.min.js" )

The implementation is as follows:

1: public static HtmlString Script(this UrlHelper helper, string contentPath)
2: {
3:     return new HtmlString (string.Format("<script type='text/javascript' src='{0}'></script>" , LatestContent(helper, contentPath)));
4: }
5:
6: public static string LatestContent(this UrlHelper helper, string contentPath)
7: {
8:     string file = HttpContext.Current.Server.MapPath(contentPath);
9:     if  (File.Exists(file))
10:     {
11:         var dateTime = File.GetLastWriteTime(file);
12:         contentPath = string.Format("{0}?v={1}", contentPath, dateTime.Ticks);
13:     }
14:
15:     return helper.Content(contentPath);
16: }

Short explanation:

  • Lines 1-4 is the extension method used in the View. These call the LatestContent method in lines 6-16.
  • Line 8 resolves the path of file on the server side and line 9 actually checks for its existence.
  • Line 11 checks for the timestamp of the file and Line 12 concats the timestamp as ticks to the query string.

Having this extension method split up into two extension methods, allowed me to add one more extension method:

1: public static HtmlString Css(this UrlHelper helper, string contentPath)
2: {
3:     return new HtmlString(string.Format("<link rel='stylesheet' type='text/css' href='{0}' media='screen' />" , LatestContent(helper, contentPath)));
4: }

So now I was also able to provide an easy solution for downloading the latest css files using the same code. So the overall code in the View looks as follows:

1: @Url.Css("~/Content/Site.css" )
2: @Url.Script("~/Scripts/jquery-1.4.4.min.js" )
About these ads
 
6 Comments

Posted by on 05/03/2011 in Software Development

 

Tags: , , ,

6 responses to “Overriding Browser Caching with ASP.NET MVC

  1. Patrick Hallisey

    25/07/2011 at 19:13

    Doesn’t using a code block to insert a script reference break javascript intellisense in Visual Studio?

     
    • evolpin

      28/07/2011 at 06:34

      I believe so. Actually, this was always the case with ASP.NET tags. For example, it’s problematic if you’d like to perform the following:

      if (status == <%= (int)MyEnum.MyStatus %>){
      }

      It also breaks intellisense.

       
  2. Hadrian

    02/08/2011 at 14:19

    Would there be any performance issues with hitting the hard disk each time? I was thinking along the same lines but maybe using assembly version instead of last file write time.

     
    • evolpin

      02/08/2011 at 18:02

      So far there wasn’t any, but I would not say that there never could be. It also depends on your server configuration (e.g. high performance drives etc.) or the number of files tested. You could always cache this data in production environment, and use a file watch to update that cache, or implement a similar solution.

       
  3. Patrick Hallisey

    02/08/2011 at 18:20

    I’m successfully using the folowing in production:

    <script type="text/javascript" src="/Scripts/Utilities.js?v=”>

    FileVersion is an extension method on UrlHelper that returns the modified date on the file to ticks leaving a src like “/Scripts/Utilities.js?v=634474592883451651″

    I didn’t use any caching, but I’d like to. If there isn’t any concern for making updates on a running site, you could just store the version tags in a static dictionary on the helper class. That way, you would only hit the file system once per javascript file per app restart, not per request.

    In our production site, we don’t really need the TimeSpan.TotalTicks() granularity that I used. We can make do with .TotalMinutes() or even .TotalDays()

    If you cache the timestamps, you’ll still have to hit Ctrl-F5 when developing as the urls will be pseudo-static after app start.

    The real beauty in all of it is the user may never have to hit Ctrl-F5 again. Just set your incriment to the smallest time you expect between website updates.

     

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

Join 60 other followers

%d bloggers like this: