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)
Hit F5 to compile and then navigate to http:// localhost:xxx/rd for the route debugger page.
Enter the URL you want to test and press Send. The results page is displayed.
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_Start\WebApiConfig.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:
- 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.
- Find all actions where the action name is “action” in the route data;
- 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.
- If the route data doesn’t contains key “action”, then the action will be searched based on the supported request method directly.
- 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.
- Eliminate all actions that are marked by the NonAction attribute.
- If more than one action matches, an HTTP 500 error is thrown. (Internally an HttpResponseException is thrown.)
- 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:publicclass MachineController : ApiController
2: {
3:public IEnumerable<Machine> Get()
4: {
5:returnnew 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:returnnew 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 }
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:
Observation
- The HTTP status code is 404 (resource not found);
- The route data contains only one key value pair, mapping “Somecode” to “Code”
- The selected route is “Api/Machine/{Code}/All” because the template fits the URL. However there are no default values defined for this route.
- 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.
Similar issue:
Example 2: Ambiguous default
Source: http://stackoverflow.com/questions/14058228/asp-net-web-api-no-action-was-found-on-the-controller
Controller
1:publicclass ValuesController : ApiController
2: {
3:// GET api/values
4:public IEnumerable<string> Get()
5: {
6:returnnewstring[] { "value1", "value2" };
7: }
8:// GET api/values/5
9:publicstring Get(int id)
10: {
11:return"value";
12: }
13:// POST api/values
14:publicvoid Post([FromBody]stringvalue)
15: {
16: }
17:// PUT api/values/5
18:publicvoid Put(int id, [FromBody]stringvalue)
19: {
20: }
21:// DELETE api/values/5
22:publicvoid Delete(int id)
23: {
24: }
25:
26: [HttpGet]
27:publicvoid Machines()
28: {
29: }
30:publicvoid 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.
Observation
- 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}
- Note that although the default mapping of “action” is “get”, the value “1” is assigned for the action, not the value “1”.
- The Values Controller is selected.
- 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.
- 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.
- 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:publicclass AccessController : ApiController
2: {
3:// GET api/access/blob
4: [HttpGet]
5:publicstring Blob()
6: {
7:return"blob shared access signature";
8: }
9:
10:// GET api/access/queue
11: [HttpGet]
12:publicstring 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: );
Observation
- 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).
- 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
- Controller selecting successfully matches the Access controller.
- 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:
- Move the default route to the end.
- 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 Tools\WebApi\RouteDebugger.
Resources
Acknowledgments
- Rick Anderson (Twitter @RickAndMSFT)
- Mike Wasson