Using Alternative Routes, How?

Last week on the stackoverflow.com forums, I saw some posts about overlapping routes. “Ideally I would like to be able to tell the routing to look for a controller named X, if it exists use that. If it doesn't exist, redirect to Y controller and pass in X as a parameter.” said the writer of the post. Such situation happens in case we want to implement dynamic routes. Something like the following routes:

routes.MapRoute( 
name="Default", 
url="{controller}/{action}", 
defaults= New {controller = "Home", action = "Index"}
)

routes.MapRoute( 
name ="Custom Page",
url ="{name}", 
defaults = New {controller = "Page", action = "Index"} 
)

In the above example, we expect the second route to be run in case there is no controller that matches the first route. In other words, the first route is responsible for handling requests that target static Controllers with static Actions. But there are also dynamic routes that must be handled by second route. The problem is, the second route never be used by route module. The above order of routes works for static controllers, but for “Custom Page”, it always tries to find a controller that matches the “name” parameter. It means it never consider the second route. If the order of the above routes changed, then the same problem occurs for static controllers.

One option to solve the mentioned problem is using a catchall route. A catchall route is a route that must be defined in the end of the route stack and it will catch all of the not captured routes. Although, a catchall route is an option for small projects, but if we deal with several dynamic routes, it doesn’t bring too much on the table. Because one has to handle all of the dynamic routes in one place and it is really a backward step to the early days of ASP.NET. It looks like to have an IHttpHandler that must handle all of the incoming requests manually!

A solution would be the ability to define an alternative route for overlapping routes. If the routing module couldn’t find a controller for the first route, then it must use the alternative route. Something like this:

var defaultRoute = routes.MapReplaceableRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

var route = routes.MapRoute(
name: "Dynamic",
url: "{*name}",
defaults: new { controller = "Dynamic", action = "Index" });

defaultRoute.AlternativeRoute = route;

Fortunately, Microsoft made a brave movement by making ASP.NET MVC open source. It brings many advantages to the community which we can’t ignore. In this post, I illustrate my implementation of the mentioned idea inspired by ASP.NET MVC source code. Before diving into the code, let’s define the problem clearly: Providing a mechanism that enables us to assign an alternative route to any route that we want. Actually, for any incoming request, the solution must check availability of the controller before processing the route, if there is no controller with the given name, then the alternative route must be used to process the request.

The first step is a customized Route that has a property to get an alternative route.

 public class CustomRoute : Route
{
/// <summary>
/// it shouldn't hide the base constructors.
/// </summary>
public CustomRoute(string url, IRouteHandler routeHandler)
: base(url, routeHandler)
{
}

/// <summary>
/// it shouldn't hide the base constructors.
/// </summary>
public CustomRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler)
{
}

/// <summary>
/// The routing will use this route, if it couldn't find a controller
for current route 
/// </summary>
public Route AlternativeRoute { get; set; }
}

The next step is finding a suitable place to hook our customized code. A nice option would be extending the MvcHandler class. That class is responsible for selecting the controller that will handle an http Request. The class has a virtual method called ProcessRequest. We can override that method to hook our code. Here is the CustomMvcHandler class.

/// <summary>
/// Extend MvcHandler to sort the Optional Parameters before
/// Processing the request
/// </summary>
class CustomMvcHandler : MvcHandler
{
/// <summary>
/// Do not hide the base-constructor
/// </summary>
/// <param name="requestContext"></param>
public CustomMvcHandler(RequestContext requestContext)
: base(requestContext)
{
}

public IRouteValueLookupService RouteValueService { get; set; }
public IRouteHandler RouteHandler { get; set; }

protected override void ProcessRequest(System.Web.HttpContext httpContext)
{
if (!this.IsControllerExist())
{
UseAlternativeRoute();
}

this.SortOptionalParameters();
base.ProcessRequest(httpContext);
}

protected override IAsyncResult BeginProcessRequest(System.Web.HttpContextBase httpContext, AsyncCallback callback, object state)
{
if (!this.IsControllerExist())
{
UseAlternativeRoute();
}

this.SortOptionalParameters();
return base.BeginProcessRequest(httpContext, callback, state);
}

protected bool IsControllerExist()
{
CustomRoute customRoute = this.RequestContext.RouteData.Route as CustomRoute;

// if AlternativeRoute is null, there is no need to do anything
if (customRoute.AlternativeRoute == null)
{
return true;
}

// Get the controller type
string controllerName = RequestContext.RouteData.GetRequiredString("controller");

ICP.CustomRouting.FromMvcLibrary.DefaultControllerFactory factory = new ICP.CustomRouting.FromMvcLibrary.DefaultControllerFactory();

var type = factory.GetControllerType(this.RequestContext, controllerName);
return (type != null);
}

protected void UseAlternativeRoute()
{
CustomRoute customRoute = this.RequestContext.RouteData.Route as CustomRoute;
this.RequestContext.RouteData = customRoute.AlternativeRoute.GetRouteData(this.RequestContext.HttpContext);
}
}

In order to resolve the controllers, CustomMvcHandler uses an extended version of the DefaultControllerFactory class. According to MSDN, “This class provides a convenient base class for developers who want to make only minor changes to controller creation”. The method that is related to our issue is called “GetControllerType”. But unfortunately, this method has protected access modifier. That is the reason that we need the following class too.

/// <summary>
/// The access modifier of the GetControllerType in DefaultControllerFactory
is
/// protected. So we have to inherit from it and make it public
/// </summary>
public class CustomControllerFactory : DefaultControllerFactory
{
public Type GetControllerType(RequestContext requestContext, string controllerName)
{
return base.GetControllerType(requestContext, controllerName);
}
}

We also need an extension “MapRoute” method and a customized MvcRouteHandler class that connects our derived MvcHandler to the CustomRoute.

/// <summary>
/// Customize MvcRouteHandler to return CustomMvcHandler instead of

/// default IHttpHandler
/// </summary>
public class CustomMvcRouteHandler : MvcRouteHandler
{
public CustomMvcRouteHandler()
: base()
{
}

public CustomMvcRouteHandler(IControllerFactory controllerFactory)
: base(controllerFactory)
{
}

public IRouteValueLookupService RouteValueService { get; set; }

protected override System.Web.IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
return new CustomMvcHandler(requestContext)
{
RouteHandler = this,
RouteValueService = this.RouteValueService
};
}
}

/// <summary>
/// Extend RouteCollection to support using CustomRoute
/// </summary>
public static class CustomRouteCollectionExtensions
{
/// <summary>
/// Create an instance of CustomRoute and register it in the given
route collection
/// </summary>
public static CustomRoute MapReplaceableRoute(this RouteCollection routes, string name, string url, object defaults)
{
CustomRoute route = new CustomRoute(url, new CustomMvcRouteHandler()
{
});

if (defaults != null)
{
route.Defaults = CreateRouteValueDictionary(defaults);
}

routes.Add(name, route);

return route;
}
}

Now, the solution is ready. I added the above codes into the RoutingExtension library in the Codeplex. You can download the source code from here.


No Comments

Post Reply