Edited
I want to cache images on the client and know that there are different ways to do it in mvc 3: (correct me if I'm wrong)
1) You can use OutputCacheAttribute
which works with the help of Expires
http header. But it will return 304 Not Modified
unless the time expire (even if the image was changed).
2) To avoid displaing stale images You can use Last-Modified
http header (with OutputCacheAttribute
). In this case the browser sends the request to the server with If-Modified-Since
http header. On the server You verify whether the object is still valid or not and if it is You just return Last-Modified
http header (and the browser takes image from the local cache); if the object was modified You return it with 200 OK
status.
So, the browser needs to send the request to the server each time before taking image from it's own cache. Here is the example -
3) There is another way (as I was told the right way in my case, cause the images will change very rarely... anyway, I need to implement exactly this): To add modified date to the image url and set caching with Expires
for the eternity (1 year or more). If the image have changed You should send new url with new version.
Here is the code:
public class LastModifiedCacheAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result is FilePathResult)
{
var result = (FilePathResult)filterContext.Result;
var lastModify = File.GetLastWriteTime(result.FileName);
if (!HasModification(filterContext.RequestContext, lastModify))
filterContext.Result = NotModified(filterContext.RequestContext, lastModify);
SetLastModifiedDate(filterContext.RequestContext, lastModify);
}
base.OnActionExecuted(filterContext);
}
private static void SetLastModifiedDate(RequestContext requestContext, DateTime modificationDate)
{
requestContext.HttpContext.Response.Cache.SetLastModified(modificationDate);
}
private static bool HasModification(RequestContext context, DateTime modificationDate)
{
var headerValue = context.HttpContext.Request.Headers["If-Modified-Since"];
if (headerValue == null)
return true;
var modifiedSince = DateTime.Parse(headerValue).ToLocalTime();
return modifiedSince < modificationDate;
}
private static ActionResult NotModified(RequestContext response, DateTime lastModificationDate)
{
response.HttpContext.Response.Cache.SetLastModified(lastModificationDate);
return new HttpStatusCodeResult(304, "Page has not been modified");
}
}
And I registered the LastModifiedCacheAttribute
in Global.asax and applied the following OutputCacheAttribute to my action method.
[HttpGet, OutputCache(Duration = 3600, Location = OutputCacheLocation.Client, VaryByParam = "productId")]
public FilePathResult GetImage(int productId)
{ // some code }
If I use the code above seems like the browser doesn't send requests to the server, instead it just take images from the cache unless the duration is not ended. (When I change the image the browser doesn't display new version)
Questions:
1) How to implement the third approach, so that the browser will take the images from client cache (and will not send the response to the server each time it wants the image) unless the image was modified?
edited: the actual code will be appreciated.
2) In the code above the time of the first image request is written to the Last-Modified (don't know why). How to write the modification date of the file into Last-Modified?
edited: this question relates to second approach. Also, if I cache only on the client and use Last-Modified
implementation I get 304 Not Modified
status only if I press F5
. If I reenter the same url I will get 200 OK
. If I cache on a client without using Last-Modified
it will always return 200 OK
no matter what. How could this be explained?
Intially used this answer but for some reason when image is modified it doesn't update in client, instead shows the cached version of image.
So the solution is using versioning which is your 3'rd option, just add LastUpdated datetime field from your product image database
Action Method
In View
Idea taken from this post.
Answer is late but Hope helps someone.
You can use the VaryByCustom option with Output Caching to achieve this without using a custom attribute. Change your method code like this:
Then add the following code to your Global.asax:
When the timestamp of the image file changes, the return value of the GetVaryByCustomString method will change which will cause ASP.NET to reload the image rather than use the cached value.
See http://msdn.microsoft.com/en-us/library/aa478965.aspx for further details.
If I understood correctly, what you are after, is having infinite caching, and rely on invalidating the cache by changing the actual url of the resource.
In that case, I believe the actual implementation is much simpler and doesn't require manual handling of headers.
Ultimately, the point is to be able to have the images loaded by a URL such as the following:
With "1" being the productId, and specifying some sort of version identifier ("v").
The key is to build that url, where the value of v is dependent on the last modification of the image (which you should probably store along with the image, or the product). You can probably hash the modification date, and use that.
If the next time you build the url, the date of the last modification is the same, you will get the same hash value, and therefore render the same url as before, and the browser will load that from cache without requesting anything from the server.
As soon as the image is updated, and that modification date changes, your code will generate a different url, which will force the browser to request it again.
Then you just apply the
OutputCache
attribute to the Action, configured to cache on the client (no need to specifyVaryByParam
) and you should be set.You could look into using ETags (http://en.wikipedia.org/wiki/HTTP_ETag), that's the first thing I thought of reading your question.
You could also have a look here: Set ETag for FileResult - MVC 3