134

I am starting to use MVC4 Web API project, I have controller with multiple HttpPost methods. The Controller looks like the following:

Controller

public class VTRoutingController : ApiController
{
    [HttpPost]
    public MyResult Route(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }

    [HttpPost]
    public MyResult TSPRoute(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }
}

Here MyRequestTemplate represents the template class responsible for handling the Json coming through the request.

Error:

When I make a request using Fiddler for http://localhost:52370/api/VTRouting/TSPRoute or http://localhost:52370/api/VTRouting/Route I get an error:

Multiple actions were found that match the request

If I remove one of the above method it works fine.

Global.asax

I have tried modifying the default routing table in global.asax, but I am still getting the error, I think I have problem in defining routes in global.asax. Here is what I am doing in global.asax.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapHttpRoute(
        name: "MyTSPRoute",
        routeTemplate: "api/VTRouting/TSPRoute",
        defaults: new { }
    );

    routes.MapHttpRoute(
        name: "MyRoute",
        routeTemplate: "api/VTRouting/Route",
        defaults: new { action="Route" }
    );
}

I am making the request in Fiddler using POST, passing json in RequestBody for MyRequestTemplate.

Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
Habib
  • 219,104
  • 29
  • 407
  • 436

12 Answers12

150

You can have multiple actions in a single controller.

For that you have to do the following two things.

  • First decorate actions with ActionName attribute like

     [ActionName("route")]
     public class VTRoutingController : ApiController
     {
       [ActionName("route")]
       public MyResult PostRoute(MyRequestTemplate routingRequestTemplate)
       {
         return null;
       }
    
      [ActionName("tspRoute")]
      public MyResult PostTSPRoute(MyRequestTemplate routingRequestTemplate)
      {
         return null;
      }
    }
    
  • Second define the following routes in WebApiConfig file.

    // Controller Only
    // To handle routes like `/api/VTRouting`
    config.Routes.MapHttpRoute(
        name: "ControllerOnly",
        routeTemplate: "api/{controller}"               
    );
    
    
    // Controller with ID
    // To handle routes like `/api/VTRouting/1`
    config.Routes.MapHttpRoute(
        name: "ControllerAndId",
        routeTemplate: "api/{controller}/{id}",
        defaults: null,
        constraints: new { id = @"^\d+$" } // Only integers 
    );
    
    // Controllers with Actions
    // To handle routes like `/api/VTRouting/route`
    config.Routes.MapHttpRoute(
        name: "ControllerAndAction",
        routeTemplate: "api/{controller}/{action}"
    );
    
Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
Asif Mushtaq
  • 13,010
  • 3
  • 33
  • 42
  • What if I don't want to set any restriction on the type of the ID? Meaning: how can I accepts string IDs as well? – frapontillo Sep 12 '13 at 07:54
  • 5
    @frapontillo: The Id should be an integeter, so that it is distiguished from the route name otherwise the routing enghine will treat it as an action name rather then an id. If you need to have the id as string then you can create an action. – Asif Mushtaq Sep 13 '13 at 11:42
  • I would use Attribute Routing instead. You won't have to use multiple routes in the WebApiConfig that way. Check out this link: https://learn.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2 – Rich Oct 23 '17 at 14:59
  • If I add like this it gives me an error ------------ namespace ImageDownloadApplication.Controllers { public class FrontModel { public string skus { get; set; } } [ActionName("ProductController")] public class ProductController : ApiController { // GET: api/NewCotroller public IEnumerable Get() { return new string[] { "value1", "value2" }; } – Umashankar Feb 16 '18 at 22:58
50

Another solution to your problem would be to use Route which lets you specify the route on the method by annotation:

[RoutePrefix("api/VTRouting")]
public class VTRoutingController : ApiController
{
    [HttpPost]
    [Route("Route")]
    public MyResult Route(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }

    [HttpPost]
    [Route("TSPRoute")]
    public MyResult TSPRoute(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }
}
Wisienkas
  • 1,602
  • 2
  • 17
  • 22
  • What Namespace is Route in? I am using MVC4 and Route is not recognized. – eaglei22 Dec 08 '15 at 19:59
  • 1
    Lookup the 2 following sources http://www.asp.net/web-api/overview/web-api-routing-and-actions/create-a-rest-api-with-attribute-routing http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2 – Wisienkas Dec 09 '15 at 21:44
  • Yes, this is the way it should go. Thanks. – newman Jul 19 '16 at 23:27
  • I have different names in my `Route` attribute, but I eventually got it to work by adding a name to one of the actions. Weird. – Mathachew Aug 12 '16 at 20:28
  • 1
    for some reason i cannot get this to work. this is exactly what i was already doing. – oligofren Nov 18 '16 at 09:17
  • 2
    How would the url look like than to call `Route` and `TSPRoute`? – Si8 Jan 20 '17 at 15:21
  • @Wisienkas why do you call it "much better" – user1451111 Nov 14 '17 at 03:38
  • For some reason, using this technique doesn't work with uploading files. I tried everything for just about a whole day to get things like ReadAsMultipartAsync() to work accepting a file, but it wouldn't work until I tried one of these other methods. It always threw a error along these lines when I tried to open the file: "Unexpected end of MIME multipart stream" – Daniel Nov 16 '18 at 22:46
  • Great man!!!, thanks!! – Hernán Garcia Sep 29 '21 at 21:45
28

use:

routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

it's not a RESTful approach anymore, but you can now call your actions by name (rather than let the Web API automatically determine one for you based on the verb) like this:

[POST] /api/VTRouting/TSPRoute

[POST] /api/VTRouting/Route

Contrary to popular belief, there is nothing wrong with this approach, and it's not abusing Web API. You can still leverage on all the awesome features of Web API (delegating handlers, content negotiation, mediatypeformatters and so on) - you just ditch the RESTful approach.

Filip W
  • 27,097
  • 6
  • 95
  • 82
  • 1
    Thanks for the answer, but it is still giving me the same error. – Habib Jul 10 '12 at 07:11
  • That's not possible, then something else must be misconfigured in your app. Can you show the entire Route setup? Also how *exactly* are you calling the controllers actions? – Filip W Jul 10 '12 at 07:22
  • The entire Route setup is in global.asax, I have post that part in my question, For making request, I am using Fiddler->Compose-> and selecting Post as the operation – Habib Jul 10 '12 at 07:30
  • try removing all other routes definition and just leave the one I posted. Then you can easily call both of the POST actions located within one controller (same as old MVC approach) – Filip W Jul 10 '12 at 07:35
  • 1
    Filip, I am using .Net framework 4.5, with mvc4 or Visual studio 2012 RC, Which template are you using to create your project, yours' is working perfectly – Habib Jul 10 '12 at 09:38
  • I did that with VS2010/.Net 4/MVC 4/WebAPI template. – Filip W Jul 10 '12 at 09:46
  • Anyway, it shouldn't matter - the only point of interest should be the route definition and the controller itself. Make sure when you call from Fiddler you call the proper URL, with proper request type, pass correct content type and request payload. Maybe it would help if you copied my dummy jQuery/controller demo to your project and try from there? – Filip W Jul 10 '12 at 09:47
  • @FilipW why do you say this "it's not a RESTful approach anymore" – user1451111 Nov 14 '17 at 03:39
13

A web api endpoint (controller) is a single resource that accepts get/post/put/delete verbs. It is not a normal MVC controller.

Necessarily, at /api/VTRouting there can only be one HttpPost method that accepts the parameters you are sending. The function name does not matter, as long as you are decorating with the [http] stuff. I've never tried, though.

Edit: This does not work. In resolving, it seems to go by the number of parameters, not trying to model-bind to the type.

You can overload the functions to accept different parameters. I am pretty sure you would be OK if you declared it the way you do, but used different (incompatible) parameters to the methods. If the params are the same, you are out of luck as model binding won't know which one you meant.

[HttpPost]
public MyResult Route(MyRequestTemplate routingRequestTemplate) {...}

[HttpPost]
public MyResult TSPRoute(MyOtherTemplate routingRequestTemplate) {...}

This part works

The default template they give when you create a new one makes this pretty explicit, and I would say you should stick with this convention:

public class ValuesController : ApiController
{
    // GET is overloaded here.  one method takes a param, the other not.
    // GET api/values  
    public IEnumerable<string> Get() { .. return new string[] ... }
    // GET api/values/5
    public string Get(int id) { return "hi there"; }

    // POST api/values (OVERLOADED)
    public void Post(string value) { ... }
    public void Post(string value, string anotherValue) { ... }
    // PUT api/values/5
    public void Put(int id, string value) {}
    // DELETE api/values/5
    public void Delete(int id) {}
}

If you want to make one class that does many things, for ajax use, there is no big reason to not use a standard controller/action pattern. The only real difference is your method signatures aren't as pretty, and you have to wrap things in Json( returnValue) before you return them.

Edit:

Overloading works just fine when using the standard template (edited to include) when using simple types. I've gone and tested the other way too, with 2 custom objects with different signatures. Never could get it to work.

  • Binding with complex objects doesn't look "deep", so thats a no-go
  • You could get around this by passing an extra param, on the query string
  • A better writeup than I can give on available options

This worked for me in this case, see where it gets you. Exception for testing only.

public class NerdyController : ApiController
{
    public void Post(string type, Obj o) { 
        throw new Exception("Type=" + type + ", o.Name=" + o.Name ); 
    }
}

public class Obj {
    public string Name { get; set; }
    public string Age { get; set; }
}

And called like this form the console:

$.post("/api/Nerdy?type=white", { 'Name':'Slim', 'Age':'21' } )
Andrew
  • 8,322
  • 2
  • 47
  • 70
  • I have tried changing the parameter types, but it seems it only allows a single Post method in the controller. Thanks for your reply – Habib Jul 10 '12 at 07:35
  • I assumed that it would try model binding to find it, since you can overload. It works with different #'s of params, though. It might not be that hard to re-write it to do this though, but they haven't released the source code yet, so I'm just stuck looking at ugly disassembly – Andrew Jul 11 '12 at 01:34
  • 2
    +1 for actually explaining the reason it's not working, and the philosophy behind web api. – MEMark Jun 04 '14 at 20:36
  • Appreciate the break down... I had assumed it was supposed to be a single POST/ PUT/ GET per controller but I wasn't sure... thus the reason why I looked it up. Since I started developing with MVC for web apps where multiple like actions per controller are the norm... it almost seems a waste so I can understand why a developer would want to. Is there a such thing as too many controllers? – Anthony Griggs Apr 30 '19 at 12:52
7

It is Possible to add Multiple Get and Post methods in the same Web API Controller. Here default Route is Causing the Issue. Web API checks for Matching Route from Top to Bottom and Hence Your Default Route Matches for all Requests. As per default route only one Get and Post Method is possible in one controller. Either place the following code on top or Comment Out/Delete Default Route

    config.Routes.MapHttpRoute("API Default", 
                               "api/{controller}/{action}/{id}",
                               new { id = RouteParameter.Optional });
Falcon
  • 79
  • 1
  • 10
Shahid Ullah
  • 153
  • 2
  • 6
3

When creating another Http Method add [HttpPost("Description")]

[HttpPost("Method1")]
public DataType Method1(MyRequestTemplate routingRequestTemplate)
{
    return null;
}

[HttpPost("Method2")]
public DataType Method2(MyRequestTemplate routingRequestTemplate){}
Pascal Ganaye
  • 1,174
  • 12
  • 28
  • 2
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 11 '22 at 15:22
2

Put Route Prefix [RoutePrefix("api/Profiles")] at the controller level and put a route at action method [Route("LikeProfile")]. Don't need to change anything in global.asax file

namespace KhandalVipra.Controllers
{
    [RoutePrefix("api/Profiles")]
    public class ProfilesController : ApiController
    {
        // POST: api/Profiles/LikeProfile
        [Authorize]
        [HttpPost]
        [Route("LikeProfile")]
        [ResponseType(typeof(List<Like>))]
        public async Task<IHttpActionResult> LikeProfile()
        {
        }
    }
}
Sushil Kumar
  • 1,401
  • 2
  • 14
  • 27
1

You can use this approach :

public class VTRoutingController : ApiController
{
    [HttpPost("Route")]
    public MyResult Route(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }

    [HttpPost("TSPRoute")]
    public MyResult TSPRoute(MyRequestTemplate routingRequestTemplate)
    {
        return null;
    }
}
Amirhossein Yari
  • 2,054
  • 3
  • 26
  • 38
0

I think the question has already been answered. I was also looking for something a webApi controller that has same signatured mehtods but different names. I was trying to implement the Calculator as WebApi. Calculator has 4 methods with the same signature but different names.

public class CalculatorController : ApiController
{
    [HttpGet]
    [ActionName("Add")]
    public string Add(int num1 = 1, int num2 = 1, int timeDelay = 1)
    {
        Thread.Sleep(1000 * timeDelay);
        return string.Format("Add = {0}", num1 + num2);
    }

    [HttpGet]
    [ActionName("Sub")]
    public string Sub(int num1 = 1, int num2 = 1, int timeDelay = 1)
    {
        Thread.Sleep(1000 * timeDelay);
        return string.Format("Subtract result = {0}", num1 - num2);
    }

    [HttpGet]
    [ActionName("Mul")]
    public string Mul(int num1 = 1, int num2 = 1, int timeDelay = 1)
    {
        Thread.Sleep(1000 * timeDelay);
        return string.Format("Multiplication result = {0}", num1 * num2);
    }

    [HttpGet]
    [ActionName("Div")]
    public string Div(int num1 = 1, int num2 = 1, int timeDelay = 1)
    {
        Thread.Sleep(1000 * timeDelay);
        return string.Format("Division result = {0}", num1 / num2);
    }
}

and in the WebApiConfig file you already have

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

Just set the authentication / authorisation on IIS and you are done!

Hope this helps!

Yawar Murtaza
  • 3,655
  • 5
  • 34
  • 40
0

Best and simplest explanation I have seen on this topic - http://www.binaryintellect.net/articles/9db02aa1-c193-421e-94d0-926e440ed297.aspx

  • Edited -

I got it working with only Route, and did not need RoutePrefix.

For example, in the controller

[HttpPost]
[Route("[action]")]
public IActionResult PostCustomer
([FromBody]CustomerOrder obj)
{
}

and

[HttpPost]
[Route("[action]")]
public IActionResult PostCustomerAndOrder
([FromBody]CustomerOrder obj)
{
}

Then, the function name goes in jquery as either -

options.url = "/api/customer/PostCustomer";

or

options.url = "/api/customer/PostCustomerAndOrder";
0

I am using .Net6. please find the following code. I have achieve like the following.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ProjectName.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class WizardAPIController : ControllerBase
    {
        [HttpGet("Methord1")]        
        public async Task<IActionResult> Methord1()
        {
            return Ok("all good");
        }

        [HttpGet("Methord2")]      
        public async Task<IActionResult> Methord2()
        {
            return Ok("all good");
        }

    }
}
Sapnandu
  • 620
  • 7
  • 9
-1
public class Journal : ApiController
{
    public MyResult Get(journal id)
    {
        return null;
    }
}

public class Journal : ApiController
{

    public MyResult Get(journal id, publication id)
    {
        return null;
    }
}

I am not sure whether overloading get/post method violates the concept of restfull api,but it workds. If anyone could've enlighten on this matter. What if I have a uri as

uri:/api/journal/journalid
uri:/api/journal/journalid/publicationid

so as you might seen my journal sort of aggregateroot, though i can define another controller for publication solely and pass id number of publication in my url however this gives much more sense. since my publication would not exist without journal itself.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
mobygeek
  • 193
  • 2
  • 14