Introduction
This article suggests an approach to organize ASP.NET MVC Routes and re-factor MapRoute method.
Background
Routing is vital to MVC. Routing defines mappings between URL and ActionMethod - that would handle request to that URL. For example, following route mapping defines that when a user hits "http://mysite/shopping/cart", call OrderController's ShowCart() method to handle this request.
Placing Routes
A common place to put route mappings is in RegisterGlobalFilters method inside Global.asax: public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
routes.MapRoute(
"Default", "{controller}/{action}/{id}", new { controller = "Login", action = "Index", id = UrlParameter.Optional } );
}
}
Often, you would use generic route "Controller/Action/Parameters" (as in aforesaid code) which maps Controller/Action/parameter with URLs tightly. But I prefer to explicitly define all routes (or at least as many as possible) to decouple URL with their handler methods for several reasons listed below:
- No matter how good your architecture is, you would find a need to re-factor it, when it grows. With Routes tightly mapped to methods, you may find difficulty in re-factoring. (without altering URLs)
- You may find a need to change routes for indexing purpose on search-engines or for any other reason as demanded by business people (without altering actions)
- In a complex scenario, you may wish to provide options to marketing team to be able to change routes and define a mechanism to map it with methods (without altering actions)
- You may wish to change URL which is attacked (without altering actions)
- Or any other reason for which you wish to alter either of URL or Method without altering other
Said that "Routes should be defined explicitly", over a period of time your routes may grow significantly. It would be wise to re-factor and organize your routes such that they are manageable and in some way logically grouped so that it's pretty easy to locate a Route.
This article is all about one such approach to organize your Routes.
Idea is, to create a Folder called "Routes" and define individual .cs files that logically segregate Routes. For example, you might have a following structure:
- Web Solution
- |_ Routes [Folder]
- |_RoutesManager.cs (More on this later)
- |_ LoginRoutes.cs
- |_ UserRoutes.cs
- |_ ProductRoutes.cs
- |_ SignleSignOnRoutes.cs
- |_ OrderRoutes
Makes Sense? I guess it is pretty neat than adding all Routes in RegisterRoutes.
Implementing Organized Routes
There are 3 components to suggested solution:
- IRouting interface: This will define one method "RegisterRoutes", that all Routing file will use to register their routes
- Routes: Individual class files that implement IRouting and define routes
- RoutesManager: Instead of explicitly calling RegisterRoutes of every class implementing IRouting, Route Manager will auto-load all routes by finding classes that implement IRouting.
Let's cover each:IRouting Interface
public interface IRouting
{
void RegisterRoutes(RouteCollection routes);
}
IRouting defines RegisterRoutes which accepts RouteCollection as argument. This RouteCollection will be the global Routes collection, MVC framework uses and each class implementing IRouting will add their routes to this RouteCollection.
Sample Implementations
LoginRoutes.cs
public class LoginRoutes : IRouting
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("loginroute", "login", new { controller = "Login", action = "Index" });
routes.MapRoute<logincontroller>("Register" x=>x.Register());*
}
}
OrderRoutes.cs
public class OrderRoutes : IRouting
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("buy", "placeorder/{id}", new { controller = "Orders", action = "Buy" });
routes.MapRoute("cart", "cart", new { controller = "Orders", action = "ViewCart" });
routes.MapRoute<OrderController>("cart/payment",r=>r.MakePayment());
<span style="font-size: 9pt;">}
</span>}
<span style="font-size: 9pt;"> </span>
Routes Manager
public class RoutesManager
{
public static void RegisterRoutes(RouteCollection routes)
{
var routings = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(x => typeof(IRouting).IsAssignableFrom(x)
&& x.IsClass).ToList();
routings.ForEach(r=>((IRouting) Activator.CreateInstance(Assembly.GetExecutingAssembly().FullName,r.FullName).Unwrap()).RegisterRoutes(routes));
}
}
We've written a generic method which search the assembly for all classes that implement IRouting and recursively call RegisterRoutes() for each such implementation.
Benefits of this approach:
- You can put your routes in an external library/assembly and modify first line to check that assembly. With this, you can easily add/update routes in other library, replace DLL and just restart app pool to have all routes updated
- 2nd line gives you an option to do dependency injection in your route class. For example, you may wish to read routes from a database or an XML file based on DI and then registers those routes.
- You're free from possible mistake of creating a route but forgetting to register it as registration of routes here is seamless. It can find Routes from anywhere you define them
Cool! We've a pretty organized and automated mechanism to add routes now. As a quick tip, we can extend MapRoute method to support much more intuitive and Visual Supported way for adding Routes.
Extending Map Routes
Default way to add Routes is:
routes.MapRoute(
"A_Unique_Key",
"{controller}/{action}/{id}", new { controller = "Login", action = "Index", id = UrlParameter.Optional }
);
When you're adding a lot of routes, there're a few things to dislike in this method:
1. You may mistakenly add same unique keys in 2 routes to identify later at runtime (which is irritating)
2. Controllers and Actions has no intellisense support, may be mistakenly typed and if someone changes controller or action name, you may miss to update routes.
As a quick tip, you may create an extension method to ease routes mapping:
public static void MapRoute<T>(this RouteCollection routes, string key="", string path = "", Expression<Func<T, ActionResult>> actionMethod = null, object defaultValues = null, object constraints = null, string[] namespaces = null) where T : IController
{
var controller = typeof(T).Name.Replace("Controller", "");
routes.MapRoute(string.IsNullOrEmpty(key)?Guid.NewGuid().ToString():key, path, new { controller = controller, action = (actionMethod == null ? "Index" : ((MethodCallExpression)actionMethod.Body).Method.Name) }, constraints, namespaces);
}
1. This extension method automatically sets action to "Index" if no action method is specified
2. Specify controller as T and action with intellisense
3. This method automatically assigns a new Guid for key. For routes, you do not expect to use RouteLink, you can skip setting key directly to avoid any duplicate key issue.
Usage Examples
1. This sets LoginController's Index() method to handle default url (http://somesite.com/)
routes.MapRoute<LoginController>("key");
2. This sets LoginController's Register() method to url "http://somesite.com/register"
routes.MapRoute<LoginController>("key","Register",x=>x.Register());
This was just a quick Tip. You can be creative to make more useful extensions.
Hope you liked my 1st article here.