Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to modify framework's default Web API routes?

It seems there is a built-in default logic for Web API to use the HTTP Verb as the action name if no action was supplied in the URL. For example, I have this route:

        config.Routes.MapHttpRoute(
            name: "DefaultApiController",
            routeTemplate: "api/{controller}"
        );

And here are my actions:

    public IEnumerable<Conference> Get()
    {
        ...
    }

    [ActionName("current")]
    public IEnumerable<Conference> GetCurrent()
    {
        ...
    }

When I go to ~/Conferences with a GET verb, it will take you to the "Get()" action. If using the POST verb, it will take you to the "Post([FromBody]Conference value)" action... and so forth. There is a conflict though when you try to go to ~/Conferences/GetCurrent (even though I have [ActionName("current")] on top):

Multiple actions were found that match the request: System.Collections.Generic.IEnumerable1[MyApp.Models.Conference] Get() on type MyApp.Api.ConferencesController System.Collections.Generic.IEnumerable1[MyApp.Models.Conference] GetCurrent() on type MyApp.Api.ConferencesController

This implies the framework is using StartsWith instead of Equal to determine a default action. Also it is ignoring the ActionName attribute when matching verb to action.

My question is how do I make the framework's default action to match to the verb exactly, instead of using StartsWith logic? A GET verb should match only a Get() action, not Get(), GetCurrent() GetPast(), etc (especially when it is ignoring the ActionName attribute).

EDIT For simplicity, I only showed one of my routes above. I think it may help if I show all my routes which is still in draft. I am trying to get a fully working REST API while still leaving room for adding my own custom actions:

    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerActionId",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: null,
            constraints: new { action = @"^[a-zA-Z]+$", id = @"^\d+$" } // action must start with character
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerActionName",
            routeTemplate: "api/{controller}/{action}/{name}",
            defaults: null,
            constraints: new { action = @"^[a-zA-Z]+$", name = @"^[a-zA-Z]+$" } // action and name must start with character
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerId",
            routeTemplate: "api/{controller}/{id}",
            defaults: null,
            constraints: new { id = @"^\d+$" } // id must be all digits
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerAction",
            routeTemplate: "api/{controller}/{action}",
            defaults: null,
            constraints: new { action = @"^[a-zA-Z]+$" } // action must start with character
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiController",
            routeTemplate: "api/{controller}"
        );

UPDATE It seems that adding HTTP verb contraints helped:

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerGet",
            routeTemplate: "api/{controller}",
            defaults: new { action = "Get" },
            constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerPost",
            routeTemplate: "api/{controller}",
            defaults: new { action = "Post" },
            constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerPut",
            routeTemplate: "api/{controller}",
            defaults: new { action = "Put" },
            constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Put) }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiControllerDelete",
            routeTemplate: "api/{controller}",
            defaults: new { action = "Delete" },
            constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Delete) }
        );
like image 583
Basem Avatar asked Jan 21 '13 16:01

Basem


People also ask

How can we configure conventional routing?

Conventional or Traditional Routing also is a pattern matching system for URL that maps incoming request to the particular controller and action method. We set all the routes in the RouteConfig file. RouteConfig file is available in the App_Start folder. We need to register all the routes to make them operational.

Which method is generally used for registering Web API routes?

Web API 2 supports a new type of routing, called attribute routing. As the name implies, attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web API. For example, you can easily create URIs that describe hierarchies of resources.


1 Answers

EDIT: Since you made a big edit to the question, I need to change the response:

In short - this will never work out of the box with Web API, because it will by default dispatch the action:

  1. based on action name if {action} is part of the route data
  2. based on HTTP verb

However, these two approaches cannot be mixed in a single controller, so you will not be able to dispatch actions using both approaches from a single controller (which is what you are trying to do).

You have three ways to fix this:

  1. rework your resources, so that you have separate ones for action-name dispatching and verb-based disptaching (which is far from ideal)

  2. register routes manually for every of the nested routes. This way you keep dispatching by HTTP verb, but routing clearly points to a specific action. You could use something like AttributeRouting (https://github.com/mccalltd/AttributeRouting) to simplify this. The downside is obviously you end up with - effectively - one route per action

  3. Implement a new IActionSelector which would allow you to mix both Verb based and action-name based dispatching in a single controller. This is the most "low-level" solution, but seems exactly like something you want to do. I posted a walkthrough last week - http://www.strathweb.com/2013/01/magical-web-api-action-selector-http-verb-and-action-name-dispatching-in-a-single-controller/

like image 127
Filip W Avatar answered Oct 15 '22 21:10

Filip W