Routing issue with overloaded Get method in WebAPI - c#

I have 2 Get methods in Product controller as below:
public IHttpActionResult Get(string keywordSearch)
[Route("api/Product/{id:long}")]
public IHttpActionResult Get(long id)
The following url works:
http://localhost:61577/api/Product?keywordSearch=test
http://localhost:61577/api/Product/1
This one fails, with message:
http://localhost:61577/api/Product/test
{
"Message": "No HTTP resource was found that matches the request URI 'http://localhost:61577/api/Product/test'.",
"MessageDetail": "No action was found on the controller 'Product' that matches the request."
}
The WebApiConfig.cs has following configurations:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Please advice, what changes should I make in API function or Config to make that work.

You would need to specify the routes in your routes configuration for both the action methods something like:
// for number type parameter only
config.Routes.MapHttpRoute(
name: "IdSearch",
routeTemplate: "api/{controller}/{id}",
defaults: null,
constraints: new { id = #"^\d+$" } // Only integers
);
and then register another one for string parameter :
// for string parameter overload action route
config.Routes.MapHttpRoute(
name: "KeyWordSearch",
routeTemplate: "api/{controller}/{keywordSearch}",
defaults: null
);
and in your action declaration apply propery attribute values on string parameter overload one, so that both will look like:
[Route("api/Product/{keywordSearch}")]
public IHttpActionResult Get(string keywordSearch)
[Route("api/Product/{id:long}")]
public IHttpActionResult Get(long id)
Another way is that you can use the RoutePrefix on your Controller class and then the Route attribute on your action methods, so that you don't have to duplicate the Prefix of Route on each action method:
[RoutePrefix("api/product")]
public class ProductController : ApiController
{
[Route("{keywordSearch}")]
public IHttpActionResult Get(string keywordSearch)
[Route("{id:long}")]
public IHttpActionResult Get(long id)
}
This should keep you going.
Hope it helps!

Related

Added a second controller to my WebAPI and it is not working

Added a second controller in my WebAPI project and it is nor working completely but the first controller is working as expected
The default URI works for the first controller to return all records:
http://localhost:59654/api/TidalBatch
The second controller does not work and returns the error in question:
http://localhost:59654/api/TidalBatchConsolidated
However, if I pass in {id} for it, it does work for when I use the id (example shown):
http://localhost:59654/api/TidalBatchConsolidated/BAM
Tried modifying the routing addresses
WebAPI config:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "TidalBatchApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "TidalBatchConsolidatedApi",
routeTemplate: "api/TidalBatchConsolidated/{id}",
defaults: new { id = RouteParameter.Optional }
);
I have 2 controllers, TidalBatchController.cs and TidalBatchConsolidatedController.cs. Both inherit from ApiController class.
Here's an example of my second controller that is not working as expected:
public class TidalBatchConsolidatedController : ApiController
{
public TidalBatchConsolidated GetAll(string id)
{
using (BDW_ProcessingEntities_TidalBatch entities = new BDW_ProcessingEntities_TidalBatch())
{
return entities.TidalBatchConsolidateds.FirstOrDefault(e => e.CompanyAbbr == id);
}
}
}
When I navigate to the base controller in the address it should return the List results in JSON format based on which entity data model is being passed in.
First, the order you register routes is important where more generic routes need to be registered after more specific routes. Secondly you more specific route needs controller in order for it to match.
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "TidalBatchConsolidatedApi",
routeTemplate: "api/TidalBatchConsolidated/{id}",
defaults: new { controller ="TidalBatchConsolidated", id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "TidalBatchApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
The controller also needs to ensure that there is a matching action
For example
public class TidalBatchConsolidatedController: ApiController {
[HttpGet]
public IHttpActionResult Get() {
//...
}
[HttpGet]
public IHttpActionResult Get(string id) {
//...
}
}
Since you have attribute routing enabled with config.MapHttpAttributeRoutes();, you could forego convention based route and use attribute routing on the controller instead
[RoutePrefix("api/TidalBatchConsolidated")]
public class TidalBatchConsolidatedController: ApiController {
//GET api/TidalBatchConsolidated
[Route("")] //Default route
[HttpGet]
public IHttpActionResult GetAll() {
//...
}
//GET api/TidalBatchConsolidated/BAM
[Route("{id}")]
[HttpGet]
public IHttpActionResult Get(string id) {
//...
}
}
Reference Attribute Routing in ASP.NET Web API 2

Multiple actions were found that match the request in webapi

I have controller with various actions and one of them is:
[HttpGet]
public IList<string> GetFoo(string id = null)
{ ... }
This action has individual route:
routes.MapHttpRoute(
name: "GetFoos",
routeTemplate: "api/my/foo/{_id}",
defaults: new { controller = "My", action = "GetFoo" }
);
When I add another action:
[HttpGet]
public IList<string> GetBar()
{ ... }
Request to localhost/api/my/foo/ fails:
Multiple actions were found that match the request:
↵System.Collections.Generic.IList`1[System.String] GetFoo(System.String) on type Controllers.MyController
↵System.Collections.Generic.IList`1[System.String] GetBar() on type Controllers.MyController"
Can someone explain please why this happens? I specified action = "GetFoo" for api/my/foo why it matches GetBar?
It could be that you configure route as following, and request without ID - /api/my/foo.
config.Routes.MapHttpRoute(
name: "GetFoos",
routeTemplate: "api/my/foo/{id}",
defaults: new {controller = "My", action = "GetFoo"}
);
// Default
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new {id = RouteParameter.Optional}
);
If so, first route doesn't match and fall throws to default route, but default route matches with multiple actions.
Note: GetFoos route will work if you request with ID explicitly - /api/my/foo/1
Ideally, if you see yourself using too many custom routes, you might want to consider using Route Attribute available in Web API 2, instead of creating individual route in Route config.
For example,
[RoutePrefix("Api/My")]
public class MyController : ApiController
{
[HttpGet]
[Route("foo/{id:int}")]
public IList<string> GetFoo(int id)
{
return new string[] {"Foo1-" + id, "Foo1-" + id};
}
[HttpGet]
[Route("bar/{id:int}")]
public IList<string> GetBar(int id)
{
return new string[] {"Bar1-" + id, "Bar1-" + id};
}
}

Web API route being ignored

I have these two routes defined:
routes.MapRoute(
name: "GetVoucherTypesForPartner",
url: "api/Partner/{partnerId}/VoucherType",
defaults: new { controller = "Partner", action = "GetVoucherTypesForPartner"}
);
routes.MapRoute(
name: "Default",
url: "api/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional}
);
In my PartnerProfile controller, I have 2 methods:
public Partner Get(string id)
{ }
public IEnumerable<string> GetVoucherTypesForPartner(string id)
{ }
If I hit the url ~/api/Partner/1234 then, as expected, the Get method is called.
However, if I hit the url ~/api/Partner/1234/VoucherType then the same Get method is called. I am expecting my GetVoucherTypesForPartner to be called instead.
I'm pretty sure something in my route setup is wrong...
You seem to have mapped standard MVC routes, not Web API routes. There's a big difference. The standard routes are used by controllers deriving from the Controller class, but if you are using the ASP.NET Web API and your controllers are deriving from the ApiController type then you should define HTTP routes.
You should do that in your ~/App_Start/WebApiConfig.cs and not inside your ~/App_Start/RouteConfig.cs.
So go ahead:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "GetVoucherTypesForPartner",
routeTemplate: "api/Partner/{partnerId}/VoucherType",
defaults: new { controller = "Partner", action = "GetVoucherTypesForPartner" }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
and then:
public class PartnerController : ApiController
{
public Partner Get(string id)
{
...
}
public IEnumerable<string> GetVoucherTypesForPartner(string partnerId)
{
...
}
}
Things to notice:
We have defined HTTP routes not standard MVC routes
The parameter that the GetVoucherTypesForPartner action takes must be called partnerId instead of id in order to respect your route definition and avoid any confusions

Multiple actions for the same HttpVerb

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.
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:
Complex Type Action Parameters and Controller Action Selection with ASP.NET Web API
Complex Type Action Parameters with ComplexTypeAwareActionSelector in ASP.NET Web API - Part 1
Complex Type Action Parameters with ComplexTypeAwareActionSelector in ASP.NET Web API - Part 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
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]+$" }
);

URL parameters in MVC 4 Web API

Let say I have two methods in MVC 4 Web API controller:
public IQueryable<A> Get() {}
And
public A Get(int id) {}
And the following route:
routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
This works as expected. Adding a one more parameter, e.g.:
public IQueryable<A> Get(int p) {}
public A Get(int id, int p) {}
leads to the situation when MVC returns 404 for the following request:
GET /controller?p=100
Or
GET /controller/1?p=100
with message "No action was found on the controller 'controller' that matches the request"
I expect that URL parameters should be wired by MVC without issues, but it is not true. Is this a bug or my misunderstanding of how MVC maps request to action?
If you think about what you're attempting to do and the routes you're trying, you'll realize that the second parameter "p" in your case, needs to be marked as an optional parameter as well.
that is your route should be defined like so:
routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{id}/{p}",
defaults: new { id = RouteParameter.Optional, p = RouteParameter.Optional });
Once you do this, the URL
/controller?p=100
will map to your
public IQueryable<A> Get(int p) {}
method and a URL like so:
/controller/1?p=100
will map to your
public A Get(int id, int p) {}
method, as you expect.
So to answer your questions....no this is not a bug but as designed/expected.
In the WebApiConfig add new defaults to the httproute
RouteParameter.Optional for the additional routes did not work for me
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}/{voucher}",
defaults: new { id = RouteParameter.Optional ,defaultroute1="",defaultroute2=""}
);

Categories