9

I have a Web API controller with the following actions:

    [HttpPut]
    public string Put(int id, JObject data)

    [HttpPut, ActionName("Lock")]
    public bool Lock(int id)

    [HttpPut, ActionName("Unlock")]
    public bool Unlock(int id)

And the following routes mapped:

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

When I make the following requests everything works as expected:

PUT /api/items/Lock/5
PUT /api/items/Unlock/5

But when I attempt to make a request to:

PUT /api/items/5

I get the following exception:

Multiple actions were found that match the request:
    Put(int id, JObject data)
    Lock(int id)
    Unlock(int id)

I tried adding an empty action name to the default route but that did not help:

[HttpPut, ActionName("")]
public string Put(int id, JObject data)

Any ideas how I can combine default RESTful routing with custom action names?

EDIT: The routing mechanism is not confused by the choice of controller. It is confused by the choice of action on a single controller. What I need is to match the default action when no action is specified. Hope that clarifies things.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Alfero Chingono
  • 2,663
  • 3
  • 33
  • 54
  • Looks like this might answer your Q: http://stackoverflow.com/questions/9499794/single-controller-with-multiple-get-methods-in-asp-net-web-api – Giscard Biamby Oct 29 '12 at 20:22
  • @GiscardBiamby Thanks for the link, I look it over an try it out. – Alfero Chingono Oct 29 '12 at 20:31
  • @GiscardBiamby You definitely pointed me in the right direction. This answer: http://stackoverflow.com/a/12185249/99373 helped me figure out what I needed to do and it worked! If you want the cred, post an answer and I'll accept. – Alfero Chingono Oct 30 '12 at 00:19

3 Answers3

6

This is an expected error from the default action selector which is the ApiControllerActionSelector. You basically have three action methods which correspond to HTTP Put verb. Also keep in mind that the default action selector considers simple action parameter types which are all primitive .NET types, well-known simple types (System.String, System.DateTime, System.Decimal, System.Guid, System.DateTimeOffset, System.TimeSpan) and underlying simple types (e.g: Nullable<System.Int32>).

As a solution to your problem I would create two controllers for those as below:

public class FooController : ApiController { 

    public string Put(int id, JObject data)
}

public class FooRPCController : ApiController { 

    [HttpPut]
    public bool Lock(int id)

    [HttpPut]
    public bool Unlock(int id)
}

the routes would look like as below:

routes.MapHttpRoute(
    name: "ApiAction",
    routeTemplate: "api/Foo/{action}/{id}",
    defaults: new { controller = "FooRPC" }
);

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

On the other hand (not completely related to your topic), I have three blog posts on action selection, especially with complex type parameters. I encourage you to check them out as they may give you a few more perspective:

tugberk
  • 57,477
  • 67
  • 243
  • 335
  • Out of curiosity, is this a case of "It can't be done that way"? I really would prefer keeping things in the same controller because it makes my application easier to maintain. There are many other controllers in the application which would need to be modified to support this proposed implementation. – Alfero Chingono Oct 29 '12 at 20:12
  • @adaptive See my updated answer. If you manage to keep action selector happy, sure you can handle them in one controller. – tugberk Oct 29 '12 at 20:17
  • Thanks for the insightful answer. So maybe I can write a custom `ActionSelector` to address the problem? – Alfero Chingono Oct 29 '12 at 20:20
  • @adaptive see the update again. I added a few blog post links. They may help as well. IMHO, you should be doing any RPC but it's just my opinion. Writing an action selector from scratch is a pain (I have been there) and the default action selector is not that extendable. – tugberk Oct 29 '12 at 20:23
  • Thanks for the great information. It will be a while before I can report back. Much appreciated!! – Alfero Chingono Oct 29 '12 at 20:34
  • @adaptive oh, fudge! It should have been "IMHO, you **shouldn't** be doing any RPC but it's just my opinion". – tugberk Oct 29 '12 at 20:37
2

With the help of Giscard Biamby, I found this answer which pointed me in the right direction. Eventually, to solve this specific problem, I did it this way:

routes.MapHttpRoute(
    name: "ApiPut", 
    routeTemplate: "api/{controller}/{id}",
    defaults: new { action = "Put" }, 
    constraints: new { httpMethod = new HttpMethodConstraint("Put") }
);

Thanks @GiscardBiamby

Community
  • 1
  • 1
Alfero Chingono
  • 2,663
  • 3
  • 33
  • 54
-3

Firstly, remove [HttpPut, ActionName("")] and then modify your route to this

config.Routes.MapHttpRoute(
    name: "Api",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { id = @"^[0-9]+$" }
    );  
Yasser Shaikh
  • 46,934
  • 46
  • 204
  • 281
  • Unfortunately, I can't do that because I have other controllers with string/guid Ids. The "numeric" constraint would break those. – Alfero Chingono Oct 29 '12 at 11:45
  • You can define multiple routes. Just change the above code to target your specific controller i.e. `routesTemplate: "api/items/{id}", defaults: new { controller = "items"...` – Ben Foster Oct 29 '12 at 19:32
  • Please see my edits above. The routing mechanism finds the controller with no problem. The problem is that it thinks all three actions are a match for the request: `PUT /api/items/5`. What I need is to match the default action when no action is specified. Hope that clarifies things. – Alfero Chingono Oct 29 '12 at 19:41