29

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

5 Answers 5

20

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)

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks a lot, just what I was looking for. It does seem kind of complicated though, shouldn't be there a "more standard" way of doing it using ASP.NET MVC? Like a method that would receive the response and the latest modification and handle it accordingly? (That's what I would do, but I don't think I'm the first person thinking about this).
I've always used my own HttpHandler - so no need to use MVC, but doesn't mean it can be done that way - just not sure which way performs better? I can recommend looking at imageresizing.net which is a great little component that can handle everything for you and more!
The problem is that my images are stored in the database and can be modified externally, therefore an HTTP handler doesn't seem like an option.
a HttpHandler can access the database or file system the same way any other executable code can - as long as the permissions are correct, etc. The HttpHandler I use does :) so your resource handling code doesn't need to be MVC specific - remember that MVC is built on top of ASP.NET
Yes, I know that, but that would mean putting application-specific code into an http handler which I don't like (I like my handlers to be generic).
17

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);

Comments

3

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 })" />

Comments

0

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..

Comments

-1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.