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.