Using Constraints for Better Routing in MVC

The default routing in a new MVC project (3 or 4) is pretty great. If I create an AppointmentController then I get a bunch of friendly URLs for free:

  • /Appointment/Index to view all appointments
  • /Appointment/Details/[id] to view the detail of the appointment with ID = [id]
  • /Appointment/Create to create a new appointment
  • /Appointment/Edit/[id] to edit the appointment with ID = [id]
  • /Appointment/Delete/[id] to (you guessed it) delete the appointment with ID = [id]

This is fantastic - a large part of the reason I love working with MVC is how easy the ASP.NET team have made it to create an application in seconds - but I don’t love the default routes. More specifically, I don’t love /Appointment/Details.

What I Want

Being unreasonably greedy, what I would really prefer is URLs like the below:

  • /Appointment/All to view all appointments
  • /Appointment/[id] to view the detail of the appointment with ID = [id]
  • Create/Edit/Delete can stay the same

Changing the Index URL is very simple - I just have to rename my controller action from Index to All (and any View that has been created), et voila: my URL is changed to /Appointment/All.

Removing the Details from the detail URL proves a little harder though.

Custom Routes

At this point, anyone who has been using MVC for more than 5 minutes will be shouting “custom routes” at the screen, so let’s give that a go. The normal means of defining a new route that doesn’t include “Details” would look something like the below:

routes.MapRoute(
	name: "NiceDetailsRoute",
	url: "{controller}/{id}",
	defaults: new { controller = "Appointment", action = "Details", id = UrlParameter.Optional }
);

Here we’re specifying a new route that is made up of the segments “controller” (e.g. Appointment) and “id” (the ID of the appointment). By specifying action = "Details" in the defaults we can remove the need to include it in the URL.

If we insert this route before the default route in either the Global.cs.asax or RouteConfig (depending on what version of MVC you are using) then it will work - we are able to use /Appointment/123 to view appointment #123. Unfortunately there is one slight problem with this approach: everything else is now broken.

Why is Everything Else Now Broken?

The problem with the route above is that the {id} segment will match anything. This means that when someone tries to browse to /Appointment/Create, MVC will assume that the Create segment of that URL is actually the ID that should be passed to the Details action on the appointment controller. Obviously it won’t be able to parse a valid integer from the string “Create” so it will fail.

What we need is some way to say “assume that the second segment is an ID but only if it looks like an ID“.

Route Constraints

Thankfully MVC comes with a nice simple way to solve this problem: constraints. We can specify a regular expression for each one of the URL segments in our route, and the route will then only be used if the regular expression matches.

In this example we only want to treat numbers as IDs so have a nice short solution:

routes.MapRoute(
	name: "NoDetails",
	url: "{controller}/{id}",
	defaults: new { controller = "Appointment", action = "Details", id = UrlParameter.Optional },
	constraints: new { id = @"^[0-9]+$" }
);

Now we can use both /Appointment/123 and /Appointment/Create without any problems.