Subtleties of MVC Routing

Published on May 16, 2012

I’ve been writing an alternative to the MVC routing mechanism for ASP.NET Web API based on prior approaches that I have worked with.  I’ll blog more about that in the future. 

Part of my motivation for writing this new router was because I didn’t know how to do what I wanted to do with MVC Routing and I knew how to do it “my way”.  Classic NIH!  However, I wanted my routing to plug in nicely to Web API and be as consistent as possible with the way MVC routing works.  Which has lead me to a much deeper understanding of how MVC routing works and how to get stuff done.

In the process I discovered a number of subtleties that I would say are not immediately apparent.  I figured it might be valuable to share them.

Assuming a controller that looks like this,

 public class CustomerController : ApiController
    {
    public HttpResponseMessage Get(int? id)
    {
        return new HttpResponseMessage()
        {
            Content = new StringContent("Here is a customer with id: " + id)
        };
    }

    [ActionName("MailingAddress")]
    public HttpResponseMessage GetMailingAddress(int? id)
    {
        return new HttpResponseMessage()
        {
            Content = new StringContent("Mailing address for customer id: " + id)
        };
    }

}

a route like this:

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    name: "test", 
    routeTemplate: "api/{controller}/{id}/{action}",
    defaults: new { id = RouteParameter.Optional, 
                    action = RouteParameter.Optional });

gives the following responses

/api/customer  => OK
/api/customer/23 => OK
/api/customer/23/Mailingaddress => OK
/api/customer/Mailingaddress => OK      // Equivalent to /api/customer with null id
/api/customer/blah => OK                // This is not great as it means it will 
					// match to anything.  Need an Id constraint
/api/customer/blah/yuck => 404

and if we add a route constraint for the id parameter,

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    name: "test", 
    routeTemplate: "api/{controller}/{id}/{action}",
    constraints: new { id = @@"d+"},
    defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional });

it gives
/api/customer => 404          	   // The Id constraint is failing even though the
					// Id is marked as optional
/api/customer/23 => OK
/api/customer/23/Mailingaddress => OK
/api/customer/Mailingaddress => 404  
/api/customer/blah => 404        
/api/customer/blah/yuck => 404

so if we adjust the Id constraint slightly,

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    name: "test", 
    routeTemplate: "api/{controller}/{id}/{action}",
    constraints: new { id = @@"d*"},
    defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional });

we get this slightly different result

/api/customer  => OK                    // The Id constraint passes 
/api/customer/23 => OK
/api/customer/23/Mailingaddress => OK
/api/customer/Mailingaddress => 404     // Even though the Id constraint passed 
					// and the id is optional, we are still not 
					// allowed to drop the segment.
/api/customer/blah => 404        
/api/customer/blah/yuck => 404