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:

	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:

	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.


12 thoughts on “Using Constraints for Better Routing in MVC

  1. Havisham's Ghost says:

    /Appointment/All/ makes sense for an API that gets all appointments, but for a web page that displays all appointments, I think /Appointments/All/ is ugly – almost as bad as a URL ending with Default.aspx. I would use /Appointments/ to show all. It makes more sense semantically, is better for SEO (your /Appointment/ would have to be set up as a redirect), and better for marketing ( reads a lot better on a podcast or radio ad).

    • Good point. You could use a similar approach to get /Appointments instead of /Appointments/All – you would need to rename the controller to be AppointmentsController instead of Appointment (which I guess makes sense for all the other URLs) and then you can update the default routing rule to default to the All action:

      name: “NoIDRoute”,
      url: “{controller}/{action}”,
      defaults: new { controller = “Appointments”, action = “All” }

  2. Is there a better way for doing, like, string-based routes? For example, if I wanted something like /news/world-celebrates-romney-election-victory, what would be the best way of doing that?

    • Assuming that you have saved that string against the story as a second unique field, it’s not too difficult – you can just specify a route like

      name: “NewsRoute”,
      url: “news/{storyName}”,
      defaults: new { controller = “News”, action = “Detail” }

      The problem is that you can’t necessarily distinguish between a story name and the name of an Action (E.g. “world-celebrates-etc” vs “Create”) using a regular expression. If you assume that a news ID will always contain a “-” character then you could set up a constraint based on that.

  3. says:

    Wonderful blog! Do you have any tips for aspiring writers?
    I’m hoping to start my own website soon but I’m a little lost on everything.

    Would you propose starting with a free platform like WordPress or go
    for a paid option? There are so many choices out there that I’m completely overwhelmed .. Any ideas? Appreciate it!

    • I would start our with a free one (I use WordPress) and then keep going until you need something more. You’ll always be able to migrate your posts to whatever new one you choose

  4. snehi says:

    I want my URL to be client-edit/Tata where Tata is the client name. However i want the id associated with it in the method that is being called. So in my view, i am using ActionLink in which i am passing clientID. How can i achieve this ? Should i be using viewbag/viewdata to store the id and pass the name as query string parameter?

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s