willvv
willvv

Reputation: 8649

ASP.NET MVC: Make browser cache images from action

I have an actionmethod that returns a File and has only one argument (an id).

e.g.

public ActionResult Icon(long id)
{
    return File(Server.MapPath("~/Content/Images/image" + id + ".png"), "image/png");
}

I want the browser to automatically cache this image the first time I access it so the next time it doesn't have to download all the data.

I have tried using things like the OutputCacheAttribute and manually setting headers on the response. i.e:

[OutputCache(Duration = 360000)]

or

Response.Cache.SetCacheability(HttpCacheability.Public);
Response.Cache.SetExpires(Cache.NoAbsoluteExpiration); 

But the image is still loaded every time I hit F5 on the browser (I'm trying it on Chrome and IE). (I know it is loaded every time because if I change the image it also changes in the browser).

I see that the HTTP response has some headers that apparently should work:

Cache-Control:public, max-age=360000

Content-Length:39317

Content-Type:image/png

Date:Tue, 31 Jan 2012 23:20:57 GMT

Expires:Sun, 05 Feb 2012 03:20:56 GMT

Last-Modified:Tue, 31 Jan 2012 23:20:56 GMT

But the request headers have this:

Pragma:no-cache

Any idea on how to do this?

Thanks a lot

Upvotes: 29

Views: 21710

Answers (5)

Huseyin Yagli
Huseyin Yagli

Reputation: 10295

Edit: I misread the question. My solution is so that the browser does not make a new request from the server on page open. If the user presses F5, the browser will request the data from the server no matter what you do about the cache info. In that case, the solution is to send an HTTP 304 as in @brodie's answer.


The easiest solution that I've found to this problem was to use OutputCacheAttribute.

For your case you must use more parameters in the OutputCache attribute:

[OutputCache(Duration = int.MaxValue, VaryByParam = "id", Location=OutputCacheLocation.Client)]
public ActionResult Icon(long id)
{
    return File(Server.MapPath("~/Content/Images/image" + id + ".png"), "image/png");
}

VaryByParam parameter makes the caching done based on the id. Otherwise, the first image will be sent for all images. You should change the Duration parameter according to your requirements. The Location parameter makes the caching only on the browser. You can set the Location property to any one of the following values: Any, Client, Downstream, Server, None, ServerAndClient. By default, the Location property has the value Any.

For detailed information read:

http://www.asp.net/mvc/overview/older-versions-1/controllers-and-routing/improving-performance-with-output-caching-cs

Upvotes: -1

prespic
prespic

Reputation: 1685

I have used the solution from @user2273400, it works, so i am posting complete solution.

This is my controller with action and temporary helping method:

using System;
using System.Web;
using System.Web.Mvc;
using CIMETY_WelcomePage.Models;

namespace CIMETY_WelcomePage.Controllers
{
    public class FileController : Controller
    {

        public ActionResult Photo(int userId)
        {
            HttpContext.Response.Cache.SetCacheability(HttpCacheability.Public);
            HttpContext.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0));

            FileModel model = GetUserPhoto(userId);


            string rawIfModifiedSince = HttpContext.Request.Headers.Get("If-Modified-Since");
            if (string.IsNullOrEmpty(rawIfModifiedSince))
            {
                // Set Last Modified time
                HttpContext.Response.Cache.SetLastModified(model.FileInfo.LastWriteTime);
            }
            else
            {
                DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince);


                // HTTP does not provide milliseconds, so remove it from the comparison
                if (TrimMilliseconds(model.FileInfo.LastWriteTime.AddMilliseconds) <= ifModifiedSince)
                {
                    // The requested file has not changed
                    HttpContext.Response.StatusCode = 304;
                    return Content(string.Empty);
                }
            }

            return File(model.File, model.ContentType);
        }

        public FileModel GetUserPhoto(int userId)
        {
            string filepath = HttpContext.Current.Server.MapPath("~/Photos/635509890038594486.jpg");
            //string filepath = frontAdapter.GetUserPhotoPath(userId);

            FileModel model = new FileModel();
            model.File = System.IO.File.ReadAllBytes(filepath);
            model.FileInfo = new System.IO.FileInfo(filepath);
            model.ContentType = MimeMapping.GetMimeMapping(filepath);

            return model;
        }

    private DateTime TrimMilliseconds(DateTime dt)
    {
        return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0);
    }

    }
}

Then the Model class:

public class FileModel
{
    public byte[] File { get; set; }
    public FileInfo FileInfo { get; set; }
    public String ContentType { get; set; }
}

And how i am using it:

<img src="@Url.Action("Photo", "File", new { userId = 15 })" />

Upvotes: 3

Calvin Nel
Calvin Nel

Reputation: 1

Just to let you know what i have uncovered and would like an explanation or more information as to how i can tell.

This: Response.Cache.SetCacheability(HttpCacheability.Public); Response.Cache.SetExpires(Cache.NoAbsoluteExpiration);

does in fact cache the images.....

to prove this... test with debugging... put break point on the image controller... and you will see that it will not be hit once... even if you F5 a couple of times... but what is odd to me is that a 200 response is still returned.

Take those lines out and you will see that you will start hitting your break point again.

My Question: What is the correct way to tell that this image is being server from cache if you still receive a 200 response.

and this was testing with IIS express 8. the built-in IIS for Visual studio is not GD..

Upvotes: 0

user2273400
user2273400

Reputation: 171

Try this code, it works for me

            HttpContext.Response.Cache.SetCacheability(HttpCacheability.Public);
            HttpContext.Response.Cache.SetMaxAge(new TimeSpan(1, 0, 0));

            Entities.Image objImage = // get your Image form your database

            string rawIfModifiedSince = HttpContext.Request.Headers.Get("If-Modified-Since");
            if (string.IsNullOrEmpty(rawIfModifiedSince))
            {
                // Set Last Modified time
                HttpContext.Response.Cache.SetLastModified(objImage.ModifiedDate);
            }
            else
            {
                DateTime ifModifiedSince = DateTime.Parse(rawIfModifiedSince);


                // HTTP does not provide milliseconds, so remove it from the comparison
                if (objImage.ModifiedDate.AddMilliseconds(
                            -objImage.ModifiedDate.Millisecond) == ifModifiedSince)
                {
                    // The requested file has not changed
                    HttpContext.Response.StatusCode = 304;
                    return Content(string.Empty);
                }
            }

            return File(objImage.File, objImage.ContentType);

Upvotes: 17

brodie
brodie

Reputation: 5434

First thing to note is that when you hit F5 (refresh) in Chrome, Safari or IE the images will be requested again, even if they've been cached in the browser.

To tell the browser that it doesn't need to download the image again you'll need to return a 304 response with no content, as per below.

Response.StatusCode = 304;
Response.StatusDescription = "Not Modified";
Response.AddHeader("Content-Length", "0");

You'll want to check the If-Modified-Since request header before returning the 304 response though. So you'll need to check the If-Modified-Since date against the modified date of your image resource (whether this be from the file or stored in the database, etc). If the file hasn't changed then return the 304, otherwise return with the image (resource).

Here are some good examples of implementing this functionality (these are for a HttpHandler but the same principles can be applied to the MVC action method)

Upvotes: 20

Related Questions