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.
/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 (domain.com/appointments 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:
routes.MapRoute(
name: “NoIDRoute”,
url: “{controller}/{action}”,
defaults: new { controller = “Appointments”, action = “All” }
);
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
routes.MapRoute(
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.
This is quite cool. I’ll try this one..
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
Thanks. Nice and clear way of teaching.
It’s difficult to find experienced people on this subject, however, you sound like
you know what you’re talking about! Thanks
Hi,
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?
Could you not lookup the client ID based on the name, or are they not unique?