I'm new to ASP.NET Web API but I'm hitting an issue where if I'm trying to add two get methods to a single controller it is failing at trying to get the action. Are we allowed only one get per controller, unless we are adding an overload of the same method?
This works if I only have one get method, e.g. the first one. As soon as I add Get to the second method name, it gives me a 500.
[HttpGet]
public string GetToday()
{
return "Hello today";
}
[HttpGet]
public string GetPending()
{
return "Hello Pending";
}
The calls I'm making:
http://abc.com/api/tasks/gettoday
http://abc.com/api/tasks/getpending
I can make this call if the method name is just 'Today', as long as I put the [HttpGet] attribute. But only if I don't have the second action. Which means if I remove the [HttpGet] attribute from the second method as well as remove 'Get' from the method name so it is just Pending, then it works.
As soon as there are two get methods I get this error:
{"Message":"An error has occurred.","ExceptionMessage":"Multiple actions were found that match the request: \r\nSystem.String TodaysTasks() on type TaskTrackerService.Controllers.TasksController\r\nSystem.String PendingTasks() on type TaskTrackerService.Controllers.TasksController","ExceptionType":"System.InvalidOperationException","StackTrace":" at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)\r\n at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)"}
The idea behind a Web API is that it represents a ReSTful resource. The semantics of ReST imply that a controller corresponds to the idea of a 'resource' and that behaviour is determined by the HTTP verb used to access the resource — means GET, POST, PUT, DELETE, …
The model used for these resources is that you treat them somewhat "CRUD-y"; that you query for elements of the resource, put new elements into the resource, update elements in the resource – you get the idea.
For each (relevant) verb you should have only one action that deals with requests of that nature. The idea of having multiple actions accessed by GET is not very ReSTful and more a MVC-ish RPC-style mode of thinking that is not well-suited for ASP.NET Web API.
So your controller might instead be written:
public class FooController : ApiControlle
{
[HttpGet]
public Account Get(string flavour)
{
switch (flavour) {
case "greeting": return "Hello today";
case "pending": return "Hello Pending";
default: throw new InvalidOperationException("Cannot get your flavour of foo :(");
}
}
}
But as you have written in a comment, the trouble was that you never intended to use the controller as Web API controller, but MVC controller.
Related
I'd like to be able to consume either posted JSON or form data at the same URL.
As things stand, I get:
fail: Microsoft.AspNetCore.Mvc.Internal.ActionSelector[1]
Request matched multiple actions resulting in ambiguity. Matching actions:
:
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HLDLB0LJCPJ4", Request id "0HLDLB0LJCPJ4:00000001": An unhandled exception was thrown by the application.
Microsoft.AspNetCore.Mvc.Internal.AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:
https://andrewlock.net/model-binding-json-posts-in-asp-net-core/ suggests using different endpoints, but I can't do that in this case.
https://massivescale.com/web-api-routing-by-content-type/ suggests a way to do it for asp.net, for example:
[ContentTypeRoute("api/test/bytype", "application/json")]
or
[ContentTypeRoute("api/test/bytype", "application/x-www-form-urlencoded")]
but in .net core, we don't have System.Web.Http.Routing. Maybe it can be ported to use Microsoft.AspNetCore.Mvc.Routing... but is there something to replace IHttpRouteConstraint
My question: is something like this already built into .net core mvc?
For example, in Java's JAX-RS, there is #Consumes("application/json")
I accomplished this by the Consumes attribute:
http://example.com/payment/callback - Accepts x-www-form-urlencoded.
[HttpPost]
[Route("callback")]
[Consumes("application/x-www-form-urlencoded")]
public ActionResult Post([FromForm] string value)
{
}
http://example.com/payment/callback - Same url but accepts application/json.
[HttpPost]
[Route("callback")]
[Consumes("application/json")]
public ActionResult Post([FromBody] JObject value)
{
}
I have my API for a small part of my application split over two controllers, because of (external) requirements on the casing of JSON data in the API (some requests should use camelCasing, while others should use PascalCasing).
Now, I have a url that I want to map with PascalCasing for GET, but camelCasing for PUT, so I tried the following:
[PascalCasing] // custom attribute, part of our code
// We configure all controllers that *don't* have this to use
// camelCasing
public class PascalCasedController : ApiController
{
[HttpGet]
[Route("url/to/my/resource/{id}")]
public IHttpActionResult(int id)
{
return Ok(GetResource(id));
}
}
public class CamelCasedController : ApiController
{
[HttpPut]
[Route("url/to/my/resource/{id}")]
public IHttpActionResult(int id, Resource resource)
{
SaveResource(id, resource);
return Ok();
}
}
The GET request works as expected, but if I try to PUT something there with Fiddler, I get the following error message:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.
The request has found the following matching controller types:
MyProject.PascalCaseController
MyProject.CamelCaseController
I realize this is probably because WebAPI maps routes to controllers first and actions next, but if HTTP methods are considered, there really isn't any ambiguity here. Is there any way that I can tell WebAPI how to do this, without having to have the methods in the same controller?
#Tomas - There's an interface "System.Web.Http.Dispatcher.IHttpControllerSelector" exposed in System.Web.Http assembly. You can use that interface and create your own HttpControllerSelector. You can then replace the DefaultControllerSelector with your custom controller selector in the HttpConfiguration during AreaRegistration.
httpConfig.Services.Replace(typeof(IHttpControllerSelector), new CustomControllerSelector(services.GetHttpControllerSelector()));
In this custom controller selector you can write your own implementation of SelectController() method of IHttpControllerSelector in which you can call GetControllerMapping() method of IHttpControllerSelector. This will give you the list of all the controllers registered. For every controller you can check for the DeclaredMethods and check for the CustomAttributes for each of the DeclaredMethods. In your case it will be either HttpGetAttribute or HttpPutAttribute.
Check the Method type of the incoming HttpRequestMessage (GET/PUT) and compare it against the value of the CustomAttributes. If you find a match of the combination of incoming request URL and the the respective Http Verb then you take that HttpControllerDiscriptor and return it from the SelectController() method..
This will allow you to have same URL with different methods in two different controllers.
I'm having trouble getting the Web API 2 attribute routing to work.
I've been trying everything I could find this whole evening but I can't find the problem.
What I want to achieve is the following:
Make a POST request to http://localhost:xxxx/api/chat/joingroup/1234 to get to the following API call:
[Route("joingroup/{id}")]
[HttpPost]
public async Task<IHttpActionResult> JoinGroup(string id, string connectionID)
{
await hubContext.Groups.Add(connectionID, id);
return Ok(hubContext.Groups.ToString());
}
This keeps getting me a http 400 message.
{"message":"No HTTP resource was found that matches the request URI 'http://localhost:41021/api/chat/joingroup/123'.",
"messageDetail":"No action was found on the controller 'Chat' that matches the request."}
But sending a post to: http://localhost:41021/api/chat/sendmessage/pm/123123 and also to http://localhost:41021/api/chat/joingroup gives me a 200
The chatcontroller:
[RoutePrefix("api/chat")]
public class ChatController : ApiController
{
IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
[...]
[Route("joingroup/{id}")]
[HttpPost]
public async Task<IHttpActionResult> JoinGroup(string id, string connectionID)
{
await hubContext.Groups.Add(connectionID, id);
return Ok(hubContext.Groups.ToString());
}
HTTP POSTS to http://localhost:xxxx/api/chat/sendmessage are working fine.
I cannot figure out why it isn't going to the correct method when I'm calling a POST on http://localhost:xxxx/api/chat/joingroup/1234.
SOLUTION:
The solution was to reference both values that are needed in the JoinGroup method, id and connectionID. Now the request will hit this method.
Using:
http://localhost:xxxx/api/chat/joingroup/john?connectionid=123 will work.
I noticed two things on the code you sent through:
the path you POST to is: localhost:xxxx/joingroup/1234 , this
should be localhost:xxxx/api/chat/joingroup/1234
because you have 2 parameters for the joingroup, you will need to pass both of them through, may be like this localhost:xxxx/api/chat/joingroup/1234?connectionID=value or you can pass it on the request body
if the connectionID is optional you can modify the method to use option al parameters like this
public string JoinGroup(string id, string connectionID = "")
please let me know if this helps.
Thanks
Ashraf
I assume the connectionID parameter references the POSTed data. The easiest thing to make it work is to decorate it with the [FromBody] attribute and put an = in front of the value being sent like this: =MyConnection1.
Web API expects an object with properties or an array otherwise. Alternatively, you can wrap the connection ID with a custom class and pass it serialized as JSON/XML.
So, say I have an existing, working page Display Cashier, which displays information about a cashier in a shop. Now, I add a button to this page that looks like:
Manager
The request-mapping for this URL maps it (successfully) to a controller: HandleGetManager
the HandleGetManager controller looks like this:
#Controller
public class HandleGetManager{
private employeeBO employeeBO; //BO handles all business logic
//spring hooks
public HandleGetManager(){}
public void setemployeeBo(employeeBO employeeBO){
this.employeeBO = employeeBO;
}
//get controller
#RequestMapping(method=RequestMethod.GET)
public String getManager(#RequestParam String cashierId){
Long managerId = employeeBO.getManagerByCashierId(cashierId);
String redirectUrl = "/displayManager.ctl?managerId=" + managerId.toString();
return redirectUrl;
}
}
Here's what happens when I try it:
I hit the new button on the Display Cashier page, I expect the following to happen:
The browser sends a get request to the indicated URL
The spring request-mapping ensures that the flow of control is passed to this class.
the #RequestMapping(method=RequestMethod.GET) piece ensures that this method is evoked
The #RequestParam String cashierId instructs Spring to parse the URL and pass the cashierId value into this method as a parameter.
The EmployeeBo has been injected into the controller via spring.
The Business logic takes place, envoking the BO and the managerId var is populated with the correct value.
The method returns the name of a different view, with a new managerId URL arg appended
Now, up until this point, everything goes to plan. What I expect to happen next is:
the browsers is directed to that URL
whereupon it will send a get request to that url,
the whole process will start again in another controller, with a different URL and a different URL arg.
instead what happens is:
this controller returns the name of a different view
The browser is redirected to a half-right, half wrong URL: handleGetManager.ctl?managerId=12345
The URL argument changes, but the name of the controller does not, despite my explicitly returning it
I get an error
What am I doing wrong? Have I missed something?
Assuming you have a UrlBasedViewResolver in your MVC configuration, the String value you return is a View name. The ViewResolver will take that name and try to resolve a View for it.
What you seem to want to do is to have a 301 response with a redirect. With view names, you do that by specifying a redirect: prefix in your view name. It's described in the documentation, here.
Here's a question/answer explaining all the (default) ways you can perform a redirect:
How can I prevent Spring MVC from doing a redirect?
Background
I wrote an REST-like API service controller for a simple Blog app. I use two routes to handle basic CRUD:
// Actions should handle: GET, POST, PUT, DELETE
routes.MapRoute("Api-SingleThing", "thing/{id}",
new { controller = "ThingService", action = "SingleThing" });
// Action should handle: GET
routes.MapRoute("Api-AllThings", "things",
new { controller = "ThingService", action = "AllThings" });
The matching controller code looks like:
[HttpGet]
public ActionResult AllThings() {}
[HttpGet]
[ActionName("SingleThing")]
public ActionResult Get(string id) {}
[HttpPost]
[ActionName("SingleThing")]
public JsonResult Create(Thing thing) {}
[HttpPut]
[ActionName("SingleThing")]
public ActionResult Update(Thing thing) {}
[HttpDelete]
[ActionName("SingleThing")]
public ActionResult Delete(int id) {}
The [ActionName()] attribute is used to avoid route constraints, so that the route when triggered always calls the "SingleThing" action on the controller - regardless of the HTTP verb. This lets the controller action methods that share the name, decide who handles the request based on the [HttpVerb] attributes.
In my blog app, this works like a charm, but only because the {id} route parameter (aka. the slug) is always present, even on POST and PUT requests.
With this new API shown above, the POST and PUT actions do not trigger the top route (ex. no {id} value), and when they trigger the second route, there is no method to handle them because of the verbs.
Question
What's the best way to maintain this REST-ful URL architecture and verb handling, and ensure I trigger my POST and PUT actions?