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 the original.jpg
file in the folder wwwroot/resized/200/120/
, but it doesn't exist, so the request passes on to the MvcMiddleware
The MvcMiddleware
invokes the ResizeImage
middleware, and saves the resized file in the folder wwwroot/resized/200/120/
. On the next request, the StaticFileMiddleware
finds the resized image in the wwwroot
folder, and serves it as usual, short-circuiting the middleware pipeline before the MvcMiddleware
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
- an IFileProvider
for the wwwroot
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)
{
var originalPath = PathString. FromUriComponent ( "/" + url) ;
var fileInfo = _fileProvider. GetFileInfo ( originalPath) ;
if ( ! fileInfo. Exists) { return NotFound ( ) ; }
var resizedPath = ReplaceExtension ( $"/resized/ { width } / { height } / { url } " ) ;
var resizedInfo = _fileProvider. GetFileInfo ( resizedPath) ;
Directory. CreateDirectory ( Path. GetDirectoryName ( resizedInfo. PhysicalPath) ) ;
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 the wwwroot
folder as we are using the WebRootFileProvider
on IHostingEnvironment
. 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 like ETag
etc - we are just serving the file using the PhysicalFileResult
. The second request is handled by the StaticFileMiddleware
. It returns the file from disk, including an ETag
and a Last-Modified
header. The third request is made with additional headers - If-Modified-Since
and If-None-Match
headers. This returns the image data with a new ETag
. Subsequent requests send the new ETag
in the If-None-Match
header, and the server responds with 304
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!