Tag Archives: Browser Cache

Overriding Browser Caching with ASP.NET MVC

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: }
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:     }
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" )

Posted by on 05/03/2011 in Software Development


Tags: , , ,