
This is the next in a series of posts on using ImageSharp to resize images in an ASP.NET Core application. I showed how you could define an MVC action that takes a path to a file stored in the wwwroot
folder, resizes it, and serves the resized file.
The biggest problem with this is that resizing an image is relatively expensive, taking multiple seconds to process large images. In the previous post I showed how you could use the IDistributedCache
interface to cache the resized image, and use that for subsequent requests.
This works pretty well, and avoids the need to process the image multiple times, but in the implementation I showed, there were a couple of drawbacks. The main issue was the lack of caching headers and features at the HTTP level - whenever the image is requested, the MVC action will return the whole data to the browser, even though nothing has changed.
In the following image, you can see that every request returns a 200 response and the full image data. The subsequent requests are all much faster than the original because we're using data cached in the IDistributedCache
, but the browser is not caching our resized image.
In this post I show a different approach to caching the data - instead of storing the file in an IDistributedCache
, we instead write the file to disk in the wwwroot
folder. We then use StaticFileMiddleware
to serve the file directly, without ever hitting the MVC middleware after the initial request. This lets us take advantage of the built in caching headers and etag behaviour that comes with the StaticFileMiddleware
.
Note: James Jackson-South has been working hard on some extensible ImageSharp middleware to provide the functionality in these blog posts. He's even written a post blog introducing it, so check it out!
The system design
The approach I'm using in this post is shown in the following figure:

With this design a request for resizing an image, e.g. to /resized/200/120/original.jpg
, would go through a number of steps:
- A request arrives for
/resized/200/120/original.jpg
- The
StaticFileMiddleware
looks for theoriginal.jpg
file in the folderwwwroot/resized/200/120/
, but it doesn't exist, so the request passes on to theMvcMiddleware
- The
MvcMiddleware
invokes theResizeImage
middleware, and saves the resized file in the folderwwwroot/resized/200/120/
. - On the next request, the
StaticFileMiddleware
finds the resized image in thewwwroot
folder, and serves it as usual, short-circuiting the middleware pipeline before theMvcMiddleware
can run. - All subsequent requests for the resized file are served by the
StaticFileMiddleware
.
Writing a resized file to the wwwroot
folder
After we first resize an image using the MvcMiddleware
, we need to store the resized image in the wwwroot
folder. In ASP.NET Core there is an abstraction called IFileProvider
which can be used to obtain information about files. The IHostingEnvironment
includes two such IFileProvder
s:
ContentRootFileProvider
- an `IFileProvider for the Content Root, where your application files are stored, usually the project root or publish folder.WebRootFileProvider
- anIFileProvider
for thewwwroot
folder
We can use the WebRootFileProvider
to open a stream to our destination file, which we will write the resized image to. The outline of the method is as follows, with preconditions, and the DOS protection code removed for brevity:`
public class HomeController : Controller
{
private readonly IFileProvider _fileProvider;
public HomeController(IHostingEnvironment env)
{
_fileProvider = env.WebRootFileProvider;
}
[Route("/resized/{width}/{height}/{*url}")]
public IActionResult ResizeImage(string url, int width, int height)
{
// Preconditions and sanitsation
// Check the original image exists
var originalPath = PathString.FromUriComponent("/" + url);
var fileInfo = _fileProvider.GetFileInfo(originalPath);
if (!fileInfo.Exists) { return NotFound(); }
// Replace the extension on the file (we only resize to jpg currently)
var resizedPath = ReplaceExtension($"/resized/{width}/{height}/{url}");
// Use the IFileProvider to get an IFileInfo
var resizedInfo = _fileProvider.GetFileInfo(resizedPath);
// Create the destination folder tree if it doesn't already exist
Directory.CreateDirectory(Path.GetDirectoryName(resizedInfo.PhysicalPath));
// resize the image and save it to the output stream
using (var outputStream = new FileStream(resizedInfo.PhysicalPath, FileMode.CreateNew))
using (var inputStream = fileInfo.CreateReadStream())
using (var image = Image.Load(inputStream))
{
image
.Resize(width, height)
.SaveAsJpeg(outputStream);
}
return PhysicalFile(resizedInfo.PhysicalPath, "image/jpg");
}
private static string ReplaceExtension(string wwwRelativePath)
{
return Path.Combine(
Path.GetDirectoryName(wwwRelativePath),
Path.GetFileNameWithoutExtension(wwwRelativePath)) + ".jpg";
}
}
The overall design of this method is pretty simple.
- Check the original file exists.
- Create the destination file path. We're replacing the file extension with
jpg
at the moment because we are always resizing to a jpeg. - Obtain an
IFileInfo
for the destination file. This is relative to thewwwroot
folder as we are using theWebRootFileProvider
onIHostingEnvironment
. - Open a file stream for the destination file.
- Open the original image, resize it, and save it to the output file stream.
With this method, we have everything we need to cache files in the wwwroot
folder. Even better, nothing else needs to change in our Startup
file, or anywhere else in our program.
Trying it out
Time to take it for a spin! If we make a number of requests for the same page again, and compare it to the first image in this post, you can see that we still have the fast response times for requests after the first, as we only resize the image once. However, you can also see the some of the requests now return a 304
response, and just 208 bytes of data. The browser uses its standard HTTP caching mechanisms on the client side, rather than caching only on the server.
This is made possible by the
etag
and Last-Modified
headers sent automatically by the StaticFileMiddleware
.
Note, we are not actually sending any caching headers by default - I wrote a post on how to do this here, which gives you control over how much caching browsers should do.
It might seem a little odd that there are three 200
requests before we start getting 304
s. This is because:
- The first request is handled by the
ResizeImage
MVC method, but we are not adding any cache-related headers likeETag
etc - we are just serving the file using thePhysicalFileResult
. - The second request is handled by the
StaticFileMiddleware
. It returns the file from disk, including anETag
and aLast-Modified
header. - The third request is made with additional headers -
If-Modified-Since
andIf-None-Match
headers. This returns the image data with a newETag
. - Subsequent requests send the new
ETag
in theIf-None-Match
header, and the server responds with304
s.
I'm not entirely sure why we need three requests for the whole data here - it seems like two would suffice, given that the third request is made with the If-Modified-Since
and If-None-Match
headers. Why would the ETag
need to change between requests two and three? I presume this is just standard behaviour though, and something I need to look at in more detail when I have time!
Summary
This post takes an alternative approach to caching compared to my last post on ImageSharp. Instead of caching the resized images in an IDistributedCache
, we save them directly to the wwwroot
folder. That way we can use all of the built in file response capabilities of the StaticFileMiddleware
, without having to write it ourselves.
Having said that, James Jackson-South has written some middleware to take a similar approach, which handles all the caching headers for you. If this series has been of interest, I encourage you to check it out!