Client side caching using Last-Modified header and

2019-04-10 11:17发布

问题:

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?

回答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



回答2:

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.



回答3:

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.



回答4:

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.