Introduction
Microsoft has given many options and extension points in MVC for making our life as a programmer easy and smooth. Custom route handlers are one of them. First of all, we will try to understand how MVC routing works, what route handlers are and why we need custom route handler. After that, we will try to understand how we can create and use custom route handlers and also what caution to take while using it. So let’s jump in.
How MVC Routing Works?
URLs are the most basic but important part of any web applications. Majorly developed as part of MVC framework, the URL routing module is a native part of ASP.NET framework now. The same component provides services to both web forms and MVC applications though through a slightly different API.
Routing HTTP Module decides which handler will be responsible to handle the request and dispatching them to the most appropriate executor. As a developer, we are not likely to deal with this module directly instead on this we need to provide it routes that our application supports.
Route Handlers
This is a class that implements IRouteHandler
interface which has only one method GetHttpHandler()
, which provides route handler class instance that will process the request. The interface looks like the following:
public interface IRouteHandler
{
IHttpHandler GetHttpHandler(RequestContext requestContext);
}
ASP.NET MVC provides a default Route Handler called as MvcRouteHandler
.
According to MSDN – “MvcRouteHandler class implements IRouteHandler, therefore it can integrate with ASP.NET routing. The MvcRouteHandler class associates the route with an MvcHandler instance. A MvcRouteHandler instance is registered with routing when you use the MapRoute method. When the MvcRouteHandler class is invoked, the class generates an MvcHandler instance using the current RequestContext instance. It then delegates control to the new MvcHandler instance.”
In a nut shell, this class returns an MVCHandler
instance based on RequestContext
that further handles the request.
Why and Where We Need a Custom Route Handler?
Well, that's a tricky question as creating any custom component can be a good practice or just a matter of architecture choice. Any software designing challenge can be handled ìn multiple ways.
- One scenario is when we want to process some data before creating the instance of the controller class. Like if we want to process any authentication header with which we need to decide whether the user has access to the system or not. We can create a Custom Route Handler which will check that header information and create either the controller which we need to invoke in case the user is authorized or the generic error controller instance which will notify user about the authentication issue. Again, this is not the way we usually handle this scenario but this can be an option.
- If we want to override the convention used by MVC. For e.g. if we want that in Controller folder, the class names should not contain a Controller suffix. We can create a Custom Route Handler which can accomplish this task though this is not the actual way through which it should get implemented.
- And finally the place where I have seen it useful. Let’s suppose we have a normal MVC website and we also use CMS pages, e.g., articles. What we want is when any request comes to server, it should first check our website routes and if none of them matches the request, it should then check our CMS page routes and then at last if nothing matches return 404. Confused? Let’s discuss it in detail.
Creating A Custom Route Handler
First of all, we will create a MVC project “CustomRouteHandler
” which will contain HomeController
in Controllers folder. We will then add RouteHandler folder which will contain two files CustomRouteHandler.cs and CustomHandler.cs. We will also add one other MVC project in the same solution and we call it CMS. This will contain ContentController
in Controllers folder. We have added both the projects in the same solution just for simplicity of this demo. In real life, these should be on separate domains.
CustomRouteHandler Project
HomeController.cs
This is the default controller file created by MVC framework. I have not made any changes in it.
CustomRouteHandler.cs
This class implements IRouteHandler
and has only one method “GetHttpHandler
”. The main purpose of this class is to return the instance of CustomHandler
class. In the constructor, we are passing the RequestContext
to the handler.
using System.Web;
using System.Web.Routing;
namespace CustomRouteHandler.RouteHandler
{
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHandler(requestContext);
}
}
}
CustomHandler.cs
This class implements the IHttpHandler
interface same as Generic handlers (.ashx). I have added one property LocalRequestContext
that will hold the value of RequestContext
object. In ProcessRequest
method, I have first tried to get the controller based on the passed controller value and in case it doesn’t exist, it will throw an exception so in catch
block I have created an instance of WebClient
and call the DownloadString
method with the URL of the CMS application. In case it fails to load the URL, it will also throw an exception so I have handled it in another catch
block and return 404 in that case. Again, I want to mention this is just a basic implementation for the sake of simplicity of this demo. In real life, we should never need to write this logic directly in the handler and also take of cross-cutting measures.
using System;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace CustomRouteHandler.RouteHandler
{
public class CustomHandler : IHttpHandler
{
public RequestContext LocalRequestContext { get; set; }
public CustomHandler(RequestContext requestContext)
{
LocalRequestContext = requestContext;
}
public void ProcessRequest(HttpContext context)
{
try
{
var controllerName = LocalRequestContext.RouteData.GetRequiredString("controller");
var controller = ControllerBuilder.Current.GetControllerFactory().
CreateController(LocalRequestContext, controllerName);
if (controller != null)
{
controller.Execute(LocalRequestContext);
}
}
catch
{
try
{
var client = new WebClient();
var content = client.DownloadString("http://localhost:24220/" +
LocalRequestContext.HttpContext.Request.FilePath);
LocalRequestContext.HttpContext.Response.Write(content);
}
catch
{
LocalRequestContext.HttpContext.Response.StatusCode = 404;
}
}
}
public bool IsReusable
{
get
{
return true;
}
}
}
}
RouteConfig.cs
In RouteConfig
class, we have changed the default route to call the custom route handler. To use the custom route handler, we need to create new Route and add it in the RouteCollection
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace CustomRouteHandler
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
RouteValueDictionary defaults = new RouteValueDictionary();
defaults.Add("controller", "Home");
defaults.Add("action", "Index");
defaults.Add("id", "");
var customRoute = new Route("{controller}/{action}/{id}",
defaults, new RouteHandler.CustomRouteHandler());
routes.Add(customRoute);
}
}
}
CMS Project
ContentController.cs
I have not done anything fancy here. This is again a basic MVC controller with only one method “Article
”.
How It Works?
Now when we load the application, as we have only added the custom route, it will call the Custom Route Handler. The route handler will then call the CustomHandler
to handle the incoming request. In case no route of current application matches the request, it will then try to get data from CMS application and if it’s not able to get the data from there also, it will return the 404 page. If you will see the URL, it is acting as the URL is part of the same application.
Conclusion
In most of the cases, the default MVCRouteHandler
will do your work but in few cases where we need some extra control over the routes, Custom Route Handlers can do the trick.