Saturday, September 19, 2009

About returning missing pictures.

While working on my pet project, I wanted to return pictures stored in the database. This is actually pretty much child’s play in ASP.NET MVC, and is well documented elsewhere.

Practically, it boils down to this:

public ActionResult Picture(int id)
{
    var item = Service.GetItem(id);

    if (item.Picture != null)
    {
        var picture = item.Picture.ToArray();
        var mime = item.PictureMime;

        return File(picture, mime);
    }
    else
    {
        return null;
    }
}

In this particular code, Service is a my data service returning an item (which is a business object in my application) and Picture is a Binary property, linked to a varbinary(MAX) column by LINQ to SQL.

All you need here is a byte array, and a mime type string, and calling File() will return a FileContentResult that takes care of everything else. Like I said, child’s play.

But there’s a couple of things missing. First of all, what happens if the picture can’t be found? What do we return? Do we throw an exception? Well, we certainly throw an exception in the Service when the item can’t be found, but nothing will point to this Action when the item doesn’t exist, so the only way the user would get this exception is by explicitly demanding this Action (and in that case, I don’t care if they get a 404).

But if the item exists, yet there is no picture, then what do we return? The sane thing to return is another picture, that visually says “Sorry, but there’s no picture.”. But returning it from the Action means we need to look it up, use Server.MapPath() to get to the file and return that instead. That means that we’ll need to call Server.MapPath() from inside the Action, and that’s bad for testability.

So what I decided to do was create an Action Filter, that would respond to null being returned from the Action method and replace the content with the missing picture. So here goes:

public class ReplaceMissingPictureAttribute : ActionFilterAttribute
{
    public string Picture { get; set; }
    public string MimeType { get; set; }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is EmptyResult)
        {
            filterContext.Result = new FilePathResult(filterContext.RequestContext.HttpContext.Server.MapPath(Picture), MimeType);
        }
        base.OnActionExecuted(filterContext);
    }
}

And we apply it like so:

[ReplaceMissingPicture(Picture = "~/Content/Images/nopicture.png", MimeType = "image/png")]
public ActionResult Picture(int id)
{
    var item = Service.GetItem(id);

    if (item.Picture != null)
    {
        var picture = item.Picture.ToArray();
        var mime = item.PictureMime;

        return File(picture, mime);
    }
    else
    {
        return null;
    }
}

So we’re still pointing to the right content to return in case null is returned, but the filter takes care of looking up the right content instead of the Action. So when we test for this, we can simply test for null being returned.

Next time I’ll discuss how I managed to make this client cacheable.

No comments:

Post a Comment