96

I have the default Route in Global.asax:

 RouteTable.Routes.MapHttpRoute(
         name: "DefaultApi",
         routeTemplate: "api/{controller}/{id}",
         defaults: new { id = System.Web.Http.RouteParameter.Optional }
         );

I wanted to be able to target a specific function, so I created another route:

RouteTable.Routes.MapHttpRoute(
         name: "WithActionApi",
         routeTemplate: "api/{controller}/{action}/{id}",
         defaults: new { id = System.Web.Http.RouteParameter.Optional }
         );

So, in my controller, I have:

    public string Get(int id)
    {
        return "object of id id";
    }        

    [HttpGet]
    public IEnumerable<string> ByCategoryId(int id)
    {
        return new string[] { "byCategory1", "byCategory2" };
    }

Calling .../api/records/bycategoryid/5 will give me what I want. However, calling .../api/records/1 will give me the error

Multiple actions were found that match the request: ...

I understand why that is - the routes just define what URLs are valid, but when it comes to function matching, both Get(int id) and ByCategoryId(int id) match api/{controller}/{id}, which is what confuses the framework.

What do I need to do to get the default API route to work again, and keep the one with {action}? I thought of creating a different controller named RecordByCategoryIdController to match the default API route, for which I would request .../api/recordbycategoryid/5. However, I find that to be a "dirty" (thus unsatisfactory) solution. I've looked for answers on this and no tutorial out there on using a route with {action} even mentions this issue.

Chait
  • 1,052
  • 2
  • 18
  • 30
Mickael Caruso
  • 8,721
  • 11
  • 40
  • 72

5 Answers5

106

The route engine uses the same sequence as you add rules into it. Once it gets the first matched rule, it will stop checking other rules and take this to search for controller and action.

So, you should:

  1. Put your specific rules ahead of your general rules(like default), which means use RouteTable.Routes.MapHttpRoute to map "WithActionApi" first, then "DefaultApi".

  2. Remove the defaults: new { id = System.Web.Http.RouteParameter.Optional } parameter of your "WithActionApi" rule because once id is optional, url like "/api/{part1}/{part2}" will never goes into "DefaultApi".

  3. Add an named action to your "DefaultApi" to tell the route engine which action to enter. Otherwise once you have more than one actions in your controller, the engine won't know which one to use and throws "Multiple actions were found that match the request: ...". Then to make it matches your Get method, use an ActionNameAttribute.

So your route should like this:

// Map this rule first
RouteTable.Routes.MapRoute(
     "WithActionApi",
     "api/{controller}/{action}/{id}"
 );

RouteTable.Routes.MapRoute(
    "DefaultApi",
    "api/{controller}/{id}",
    new { action="DefaultAction", id = System.Web.Http.RouteParameter.Optional }
);

And your controller:

[ActionName("DefaultAction")] //Map Action and you can name your method with any text
public string Get(int id)
{
    return "object of id id";
}        

[HttpGet]
public IEnumerable<string> ByCategoryId(int id)
{
    return new string[] { "byCategory1", "byCategory2" };
}
Chris Li
  • 3,715
  • 3
  • 29
  • 31
  • 1
    I tried the advice above and everything works as expected. Thank you very much for that "secret". – Mickael Caruso Nov 28 '12 at 14:55
  • On point 2 in your answer, if `id` is optional then URL like `/api/{part1}/{part2}` may still go into `DefaultApi` route if no matching action is found for `WithActionApi` route. Please correct me if I'm wrong. – orad Feb 21 '15 at 02:12
  • what happens if I want to have api/{controller}/{action} only. without id. If someone else is facing same issue. forget about WebApiConfig. read about Attribute Routing. – ahsant Oct 15 '19 at 06:02
41

You can solve your problem with help of Attribute routing

Controller

[Route("api/category/{categoryId}")]
public IEnumerable<Order> GetCategoryId(int categoryId) { ... }

URI in jquery

api/category/1

Route Configuration

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

and your default routing is working as default convention-based routing

Controller

public string Get(int id)
    {
        return "object of id id";
    }   

URI in Jquery

/api/records/1 

Route Configuration

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Review article for more information Attribute routing and onvention-based routing here & this

MSTdev
  • 4,507
  • 2
  • 23
  • 40
  • god answer. but can't we add for all, api/{controller}/{action}/{id} along with api/{controller}/{id} ? – karim Nov 26 '14 at 11:16
  • Attribute routing definitly solves the problem. One important point: Prior to Web API 2, the Web API project templates generated code like this: protected void Application_Start() { WebApiConfig.Register(GlobalConfiguration.Configuration); }If attribute routing is enabled, this code will throw an exception. If you upgrade an existing Web API project to use attribute routing, make sure to update this configuration code to the following: protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); } – Deeb Jan 26 '15 at 12:08
0

Try this.

public class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        var json = config.Formatters.JsonFormatter;
        json.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
        config.Formatters.Remove(config.Formatters.XmlFormatter);

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional , Action =RouteParameter.Optional }

        );
    }
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Amit
  • 1
  • 1
0

The possible reason can also be that you have not inherited Controller from ApiController. Happened with me took a while to understand the same.

0

To differentiate the routes, try adding a constraint that id must be numeric:

RouteTable.Routes.MapHttpRoute(
         name: "DefaultApi",
         routeTemplate: "api/{controller}/{id}",
         constraints: new { id = @"\d+" }, // Only matches if "id" is one or more digits.
         defaults: new { id = System.Web.Http.RouteParameter.Optional }
         );  
Derek Wade
  • 697
  • 8
  • 11