4

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?

Community
  • 1
  • 1
Aleksei Chepovoi
  • 3,695
  • 7
  • 34
  • 73

4 Answers4

1

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

Community
  • 1
  • 1
Alex Paven
  • 5,467
  • 2
  • 19
  • 33
  • Thanks for links. I think ETag works just the same as the Last-Modified. The only difference is that the Last-Modified is a date, but Etag is an identifier. I've already implemented windows service app that watches for changes in the images folder and updates the DB. So, I only need some ajax to implement 3rd approach (1st question). Do You know how to do it so when the DB entry changes the image will change automatically (ajax)? And still wondering about 2d question. – Aleksei Chepovoi Feb 26 '13 at 10:30
  • 1
    @AlekseiChepovoi - ajax as in refreshing the image without the user doing anything? Two options really - polling periodically or websockets, something like nodejs. As for the second question I have no idea - what browsers did you test it on? Maybe only some browsers are behaving weirdly, I know for sure that IE does with respect to caching... Welp, sorry I can't be of much more help – Alex Paven Feb 26 '13 at 10:59
0

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:

http://someDomain/product/image/1?v={something}

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 specify VaryByParam) and you should be set.

Pablo Romeo
  • 10,870
  • 1
  • 27
  • 57
  • I store images on the filestream, so I think I can't store modified date with image, how can I get it? I used File.GetLastWriteTime but it doesn't work: for some reason (even if I don't use Last-Modified filter implementation) the LastModified header contains the "last access" date instead of the "last modified" date. – Aleksei Chepovoi Feb 18 '13 at 21:36
  • 1
    You mean on the actual file system? Well, GetLastWriteTime should work, if you have adequate permissions. If you debug the code, you see an invalid value for GetLastWriteTime? Another option may be to store the image upload date, along with some associated entity, like the Product for example. That could also work. – Pablo Romeo Feb 18 '13 at 21:40
  • yes, actual file stream. In the code above I write this modification date into Last-Modified header. But every time I reload the page I get If-Modified-Since in request equal to Last-Modified from the previous response?! What can be the reason for this? p.s. I'll create additional property for modified date in related Entity, but this strange Last-Modified behavior bothers and annoys me very much :) – Aleksei Chepovoi Feb 18 '13 at 21:49
  • To be honest, I'd just get rid of that action filter. I don't know what value it would have if you are implementing approach #3. – Pablo Romeo Feb 18 '13 at 22:21
  • do I need to build the url mannualy or I can just pass the version as a parameter to my GetImage() action method? can You provide some code? – Aleksei Chepovoi Feb 19 '13 at 08:00
  • Either way should be fine. Unfortunately I don't have any code examples, since there isn't much code too it. It's just a regular url with an extra parameter that changes in value according to the version of the content. – Pablo Romeo Feb 19 '13 at 13:10
  • I mean where to change the value? I can't figure out where to set the lastModifiedProperty so it would be always updated: in the GetImage()? in the productRepository? So that I can just pass it as a parameter to GetImage() method. And how to do that this property will update automatically? Thanks in advance. – Aleksei Chepovoi Feb 19 '13 at 14:04
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/24770/discussion-between-pablo-romeo-and-aleksei-chepovoi) – Pablo Romeo Feb 19 '13 at 14:32
0

You can use the VaryByCustom option with Output Caching to achieve this without using a custom attribute. Change your method code like this:

[HttpGet, OutputCache(Duration = 3600, 
    Location = OutputCacheLocation.Client, 
    VaryByCustom = "imagedate")]
public FilePathResult GetImage(int productId)
{ // some code }

Then add the following code to your Global.asax:

    public override string GetVaryByCustomString(System.Web.HttpContext context, string custom)
    {
        if (custom.ToLower() == "imagedate")
        {
            return System.IO.File.GetLastWriteTime(Server.MapPath("~/Images/my-image.png")).ToString();
        }
        else
        {
            return base.GetVaryByCustomString(context, custom);
        }
    }

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.

Paul Taylor
  • 5,183
  • 4
  • 39
  • 62
0

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

    [HttpGet]
    [OutputCache(
    Duration = 7200,
    VaryByParam = "productId;lastUpdated",
    Location = OutputCacheLocation.Client)]
    public ActionResult GetImage(string productId, string lastUpdated)
    {
        var dir = Server.MapPath("~/productimages/");
        var path = Path.Combine(dir, productId + ".jpg");
        return base.File(path, "image/jpeg");
    }

In View

<img src="@Url.Action("GetImage", "Home", new { productId = "test-product-100", 
lastUpdated =Model.LastUpdated })" />

Idea taken from this post.

Answer is late but Hope helps someone.

Community
  • 1
  • 1
Shaiju T
  • 5,220
  • 15
  • 91
  • 175