Most of the browser caching issues can be resolved by viewing the response headers (can be done in Google chrome developer tools).
Now the clientCache
section of your web.config
file should set your output caching to a maximum age as you see in the image below has set the max-age
to 86400
which is 1 day in seconds.
Here is the web.config snippet for this setup.
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />
Now thats great, the response header has a max-age
property set on the Cache-Control
header. So the browser should be caching the content. Well this is mostly true but some browsers require another flag to be set. Specifically the public
flag set for the cache control header. This can be easily added by using the cacheControlCustom
attribute in the web.config
. Here is an example.
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />
Now when we retry the page and inspect the headers.
Now as you can see from the image above we now have the value public, max-age=86400
. So our browser has all it needs to cache the resources. Now examining the headers and the network tab of google chrome will help us.
Here is the first request to the file.. Note the file is not cached...
Now lets navigate back to this page (NOTE: do not refresh the page, we will talk about that in a second). You will see the response in now returning from cache (as circled).
Now what happens if I refresh the page using either F5 or using the browser refresh feature. Wait.. where did the (from cache)
go.
Well in Google Chrome (not sure about other browsers) using the refresh button will re-download the static resources regardless of the cache header (insert clarification here please). That means that the resources has been re-retrieved and the max age header sent over.
Now after all the explanation above, be sure to test how you are monitoring the cache headers.
Update
Based on your comments you stated you have a generic handler (IHttpHandler
) named Image.ashx
with the content type of image/jpg
. Now you may expect the default behaviour would be to cache this handler. However IIS sees the extension .ashx
(correctly) as a dynamic script and is not subject to caching without explicitly setting the cache headers in the code itself.
Now this is where you need to be careful, as typically IHttpHandlers
should infact not be cached as they usually deliver dynamic content. Now if that content is unlikely to change you could set your cache headers directly in the code. Here is an example of setting cache headers in IHttpHandlers
using the Response
context.
context.Response.ContentType = "image/jpg";
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);
context.Response.TransmitFile(context.Server.MapPath("~/out.jpg"));
Now looking at the code we are setting a few properties on the Cache
property. To get the desired response I have set the properties.
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
tells the out put cache to set the max-age=
part of the Cache-Control
header to be 1
day in the future (86400 seconds).
context.Response.Cache.SetCacheability(HttpCacheability.Public);
tells the out put cache to set the Cache-Control
header to public
. This is quite important as it tells the browser to cache to object.
context.Response.Cache.SetSlidingExpiration(true);
tells the output cache to ensure it is setting the max-age=
part of the Cache-Control
header properly. Without setting the sliding expiration, the IIS out put caching will ignore the max age header. Putting this together gives me this result.
As I stated above you may not want to cache the .ashx
files as they typically deliver dynamic content. However if that dynamic content is not likely to change in a given period you can use the methods above to deliver your .ashx
file.
Now in conjunction with the process listed above you could also set the ETag (see wiki) component of the cache headers so the browser can verify the content being delivered by a custom string. The wiki states:
An ETag is an opaque identifier assigned by a web server to a specific
version of a resource found at a URL. If the resource content at that
URL ever changes, a new and different ETag
is assigned.
So this is really some sort of unique identification for the browser to identify the content being delivered in the response. By providing this header the browser on the next reload will send over a If-None-Match
header with the ETag
from the last response. We can modify our handler to detect the If-None-Match
header and compare it to our own generated Etag
. Now there is no exact science to generating ETags
but a good rule of thumb is to deliver an identifier that will most likely define only one entity. In this case I like to use two strings concatenated together such as.
System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/saveNew.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();
In the snippet above we are loading a file from our file system (you could get this from anywhere). I am then using the GetHashCode()
method (on all objects) to get the integer hash code of the object. In the example I concat the hash of the file name, then the last write date. The reason for the last write date is in case the file is changed the hash code is changed as well, thus making the finger prints different.
This will generate a hash code similar to 306894467-210133036
.
So how do we use this in our handler. Below is the newly modified version of the handler.
System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/out.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();
var browserETag = context.Request.Headers["If-None-Match"];
context.Response.ClearHeaders();
if(browserETag == eTag)
{
context.Response.Status = "304 Not Modified";
context.Response.End();
return;
}
context.Response.ContentType = "image/jpg";
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);
context.Response.Cache.SetETag(eTag);
context.Response.TransmitFile(file.FullName);
As you can see, I have changed quite alot of the handler however you will notice that we generate the Etag
hash, check for an incoming If-None-Match
header. If the etag hash and the header are equal then we tell the browser that the content hasnt changed by returning the status code 304 Not Modified
.
Next was the same handler except we add the ETag
header by calling:
context.Response.Cache.SetETag(eTag);
When we run this up in the browser we get.
You will see from the image (as i did change the file name) that we now have all the components of our cache system in place. The ETag
is being delivered as a header, and the browser is sending the request header If-None-Match
so our handler can respond accordingly to cache file changed.