April 4th, 2013

Debugging ASP.NET Web API with Route Debugger

Tutorial and Tool written by Troy Dai (Twitter @troy_dai) with assistance from Rick Anderson (Twitter @RickAndMSFT)

Search for “asp.net web api routing” on stackoverflow, you’ll find many questions. How exactly does Web API routing work? Why doesn’t my route work? Why is this action not invoked? Often time it is difficult to debug route.

To address this issue I wrote this tool named “ASP.NET Web API Route Debugger” trying to make Web API developers’ lives a bit easier.

In this article I’ll first introduce stdebugger. Then I’ll introduce how routing works in Web Api. It is followed by three examples of how to use the route debugger in real cases.

How to step up the Route Debugger

You can install Route Debugger from NuGet (http://www.nuget.org/packages/WebApiRouteDebugger/)

   1: PM> Install-Package WebApiRouteDebugger

The NuGet package will add a new area and to your project. The image below shows the new files added to the project. (The + icon shows new files and the red check icon shows changed files)

clip_image002

Hit F5 to compile and then navigate to http:// localhost:xxx/rd for the route debugger page.

clip_image004

Enter the URL you want to test and press Send. The results page is displayed.

clip_image006

I’ll explain how to read the results in the following sections.

How does routing works in ASP.NET Web Api

The routing mechanism of ASP.NET Web API is composed of three steps: find the matching route and parse the route data, find the matching controller, and find the matching action. In any step fails to find a selection the steps following will not be executed. For example, if no controller is found, the matching ends and no action is looked for.

In the first step, a route will be matched. Every route is defined with route template, defaults, constraints, data tokens and handler. (Routes are configured by default in App_StartWebApiConfig.cs ) Once a route is matched, the request URL is parsed into route data based on the route template. Route data is a dictionary mapping from string to object.

Controller matching is purely done based on the value of “controller” key in route data. If the key “controller” doesn’t exist in route data, controller selection will fail.

After controller is matched, all the public methods on the controller are found through reflection. To match the action, it uses the following algorithm:

clip_image008

  1. If route data contains key “action”, then the action will be searched based on action name. Unlike ASP.NET MVC, Web API routes generally do not use action names in routing.
    1. Find all actions where the action name is “action” in the route data;
    2. Each action supports one or more HTTP Verbs (GET, POST, PUT, etc.). Eliminate those actions which don’t support the current HTTP request’s verb.
  2. If the route data doesn’t contains key “action”, then the action will be searched based on the supported request method directly.
  3. For selected actions in either of above two steps, examine the parameters of action method. Eliminate those actions that don’t match all the parameters in the route data.
  4. Eliminate all actions that are marked by the NonAction attribute.
    1. If more than one action matches, an HTTP 500 error is thrown. (Internally an HttpResponseException is thrown.)
    2. If there is no matching action, an HTTP 404 error is thrown.

How to use the Route bugger

Example 1: Missing Controller Value

Source: http://stackoverflow.com/questions/13876816/web-api-routing

Issue

The controller and routes are shown below. The URL doesn’t match the MachineApi route.

   1: localhost/api/machine/somecode/all

 

You can download Sample1 and install the route debugger NuGet package to follow along.

Controller

   1: public class MachineController : ApiController

   2: {

   3:     public IEnumerable<Machine> Get()

   4:     {

   5:         return new List<Machine>{

   6:             new Machine    {

   7:                 LastPlayed = DateTime.UtcNow,

   8:                 MachineAlertCount = 1,

   9:                 MachineId = "122",

  10:                 MachineName = "test",

  11:                 MachinePosition = "12",

  12:                 MachineStatus = "test"

  13:             }

  14:         };

  15:     }

  16:  

  17:     public IEnumerable<Machine> All(string code)

  18:     {

  19:         return new List<Machine>

  20:         {

  21:          new Machine

  22:             {

  23:                 LastPlayed = DateTime.UtcNow,

  24:                 MachineAlertCount = 1,

  25:                 MachineId = "122",

  26:                 MachineName = "test",

  27:                 MachinePosition = "12",

  28:                 MachineStatus = "test"

  29:             }

  30:         };

  31:     }

  32: }

Route

   1: config.Routes.MapHttpRoute(

   2:     name: "MachineApi",

   3:     routeTemplate: "api/machine/{code}/all"

   4: );

   5:  

   6: config.Routes.MapHttpRoute(

   7:     name: "DefaultApi",

   8:     routeTemplate: "api/{controller}/{id}",

   9:     defaults: new { id = RouteParameter.Optional }

Test

Test http://localhost/api/machine/somecode/all in the route debugger:

clip_image010

Observation

  1. The HTTP status code is 404 (resource not found);
  2. The route data contains only one key value pair, mapping “Somecode” to “Code”
  3. The selected route is “Api/Machine/{Code}/All” because the template fits the URL. However there are no default values defined for this route.
  4. No controller matches (none of the rows are highlighted in controller selecting table)

Analysis

The route debugger output shows the “controller” value is not found in the route data or route defaults. The default controller selector relies on “controller” value to find a proper controller.

A common misunderstanding of route templates is that the values are mapped based on their position. That’s not true. In this case even though Machine is placed right after Api, there is no hint that this segment of URL should be picked up.

Solution

Add a default value specifying the machine controller to the first route:

   1: config.Routes.MapHttpRoute(

   2:     name: "MachineApi",

   3:     routeTemplate: "api/machine/{code}/all",

   4:     defaults: new { controller = "Machine" });

After this change, you get an HTTP 200 return, the machine controller is matched and the Action is matched. The matching route, controller and action are highlighted in green in the route debugger as shown below.

clip_image012

Similar issue:

http://stackoverflow.com/questions/13869541/why-is-my-message-handler-running-even-when-it-is-not-defined-in-webapi

Example 2: Ambiguous default

Source: http://stackoverflow.com/questions/14058228/asp-net-web-api-no-action-was-found-on-the-controller

Controller

   1: public class ValuesController : ApiController

   2: {

   3:     // GET api/values

   4:     public IEnumerable<string> Get()

   5:     {

   6:         return new string[] { "value1", "value2" };

   7:     }

   8:     // GET api/values/5

   9:     public string Get(int id)

  10:     {

  11:         return "value";

  12:     }

  13:     // POST api/values

  14:     public void Post([FromBody]string value)

  15:     {

  16:     }

  17:     // PUT api/values/5

  18:     public void Put(int id, [FromBody]string value)

  19:     {

  20:     }

  21:     // DELETE api/values/5

  22:     public void Delete(int id)

  23:     {

  24:     }

  25:  

  26:     [HttpGet]

  27:     public void Machines()

  28:     {

  29:     }

  30:     public void Machines(int id)

  31:     {

  32:     }

  33: }

Route definition

   1: config.Routes.MapHttpRoute(

   2:     name: "DefaultApi",

   3:     routeTemplate: "api/{controller}/{action}/{id}",

   4:     defaults: new { action = "get", id = RouteParameter.Optional });

Test

Try the following three routes which work correctly

  • /api/Values
  • /api/Values/Machines
  • /api/Values/Machine/100

However the URL /api/Values/1 Returns a 404 error.

clip_image014

Observation

  1. In the route data section you can see “action” is mapped to “1”, the third segment in the URL. It is a restriction assignment since the selected route is api/{controller}/{action}/{id}
  2. Note that although the default mapping of “action” is “get”, the value “1” is assigned for the action, not the value “1”.
  3. The Values Controller is selected.
  4. The Action selecting table has no match. The “By Action Name” column is filled with “False”, which means all actions are rejected because their action names are not matched to the “action” value in route data.

Analysis

There are two pivots here.

  1. The route data will always prefer the value in URL over the default value if a URL value can be found. In all of the four URLs listed above, none of them matches the default “action” mapping. In all four URLs, the route data contains the action key and action value.
  2. Because the “action” value exists in the route data, the action selector will pick the action from the route data.

The route debugger tool shows that with the URL http://localhost:xxx/api/values/1, “1” is the action name and no such action exits.

Solution

Use one strategy of action matching, either by action name or by verb. Don’t put both in one controller and one route.

Example 3: Ambiguous Action

Source: Why don’t my routes find the appropriate action? http://stackoverflow.com/questions/14614516/my-web-api-route-map-is-returning-multiple-actions

Issue

The URL cause 500 is http://localhost/api/access/blob

Controller

   1: public class AccessController : ApiController

   2: {

   3:     // GET api/access/blob

   4:     [HttpGet]

   5:     public string Blob()

   6:     {

   7:         return "blob shared access signature";

   8:     }

   9:  

  10:     // GET api/access/queue

  11:     [HttpGet]

  12:     public string Queue()

  13:     {

  14:         return "queue shared access signature";

  15:     }

  16: }

Route definition

   1: config.Routes.MapHttpRoute(

   2:     name: "DefaultApi",

   3:     routeTemplate: "api/{controller}/{id}",

   4:     defaults: new { id = RouteParameter.Optional }

   5: );

   6:  

   7: config.Routes.MapHttpRoute(

   8:     name: "AccessApi",

   9:     routeTemplate: "api/{controller}/{action}"

  10: );

clip_image016

Observation

  1. There are two routes and the URL matches both routes. The first one is selected because Web API routing selects the first route that matches (Greedy matching).
  2. The first route template doesn’t contain {action}, there isn’t “action” value in route data, therefore the action will be selected based on HTTP verb
  3. Controller selecting successfully matches the Access controller.
  4. Two actions are selected using the only available matching criteria, the HTTP verb GET

Analysis

The root problem is that both routes match and the first one is picked while the developer was expecting the second route to match. The “action” name is ignored and eventually action selector tries to match action based on verb alone.

Solution

Two solutions:

  1. Move the default route to the end.
  2. Delete default route.

The greedy route selection can lead to difficult to resolve errors, especially when you assume the wrong route was selected. The route debugger is especially useful for this problem, as it shows you the route template selected.

Conclusion

Web API routing problems can get tricky and be difficult to diagnose. The Route Debugger tool can help you find routing problems and understand how routing works. We plan to address routing problems in the future (at least partially) with Attribute routing in Web API.

Source Code

The tool’s source code is available. You can also download the source to the route debugger http://aspnet.codeplex.com. Click the Source Code tab and expand ToolsWebApiRouteDebugger.

clip_image017

Resources

Acknowledgments

  1. Rick Anderson (Twitter @RickAndMSFT)
  2. Mike Wasson
Category
ASP.NET

Author

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Stewart .

    Hi Rick, a fab tutorial as usual :-).
    Would it be a good idea to include something about Ajax and remote website routing in your article?

    I have a Controller/Action call from Jquery Ajax. It works fine on localhost (and when published to Azure and AWS) but not when published to 1&1.
    Can this tool route debugger be used to check routing for a hosted website? Fiddler shows the 'normal' MVC controller/Actions as WebApp/Controller/Action but...

    Read more