Custom Error Pages in NancyFx
11 Apr 2013NancyFx (commonly just called Nancy) is a Sinatra inspired MVC framework for ASP.Net. It’s low ceremony and just plain fun to write in. The home page communicates volumes about its philosophy in just 7 lines of code (including braces).
One of the things I like about this framework is how easy it is to override default behavior. Here’s the default 404 page.
Cute, but we had a customer that wanted something a bit more “Corporate”.
For many operations in Nancy you can simply implement an interface and the framework will incorporate it at runtime. Implementing a custom error page is a good example of how, “Things just work”.
Editors Note: See updates at the the end of this post.
public class StatusCodeHandler : IStatusCodeHandler
{
private readonly IRootPathProvider _rootPathProvider;
public StatusCodeHandler(IRootPathProvider rootPathProvider)
{
_rootPathProvider = rootPathProvider;
}
public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context)
{
return statusCode == HttpStatusCode.NotFound;
}
public void Handle(HttpStatusCode statusCode, NancyContext context)
{
context.Response.Contents = stream =>
{
var filename = Path.Combine(_rootPathProvider.GetRootPath(), "content/PageNotFound.html");
using (var file = File.OpenRead(filename))
{
file.CopyTo(stream);
}
};
}
}
Notice there isn’t any code to register the new handler. Simply adding a class that implements IStatusCodeHandler
is all it takes.
Let’s look at the individual methods.
public StatusCodeHandler(IRootPathProvider rootPathProvider)
{
_rootPathProvider = rootPathProvider;
}
IRootPathProvider
is a standard Nancy interface that has one method, GetRootPath()
returns the root path of where the web site is installed on the server. Nancy provides a default implementation of this interface.
Nancy’s IoC container (TinyIoC) does the rest. It locates the StatusCodeHandler
class and using constructor injection, creates an instance of StatusCodeHandler
with the required IRootPathProvider
instance. That’s a mouthful and if you’ve never worked with IoC’s, feels a bit magical. It’s also an excellent example of how IoC’s can simplify your code.
public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context)
{
return statusCode == HttpStatusCode.NotFound;
}
The rest is easier to understand. The HandleStatusCode()
method checks if status code is HttpStatusCode.NotFound
. Returning true
tells Nancy that our StatusCodeHandler
will handle the response to this status code.
And finally, the Handle()
method generates the new error page. Easy-Schmeasy.
public void Handle(HttpStatusCode statusCode, NancyContext context)
{
context.Response.Contents = stream =>
{
var filename = Path.Combine(_rootPathProvider.GetRootPath(), "content/PageNotFound.html");
using (var file = File.OpenRead(filename))
{
file.CopyTo(stream);
}
};
}
The content is streamed from a file which is why I needed the root-path to locate it.
Update
Based on feedback there’s a better way to implement this class. Here’s the updated code.
public class PageNotFoundHandler : DefaultViewRenderer, IStatusCodeHandler
{
public PageNotFoundHandler(IViewFactory factory) : base(factory)
{
}
public bool HandlesStatusCode(HttpStatusCode statusCode, NancyContext context)
{
return statusCode == HttpStatusCode.NotFound;
}
public void Handle(HttpStatusCode statusCode, NancyContext context)
{
var response = RenderView(context, "PageNotFound");
response.StatusCode = HttpStatusCode.NotFound;
context.Response = response;
}
}
Using this method has a couple of advantages.
- No longer requires a reference to
IRootPathProvider
RenderView()
allows the inclusion of model data (not used in this example). The model data could include error details for instance.- Invokes the view engine which allows for template expansions.
Note that the PageNotFound template will have to be located with your other views.