Learning a lot about MapRouting and have spent WAY too much time trying to get what I need done, so I'm hoping someone can help out. :)
I'm looking to do the following:
/api/Entities/1 <- Views the details entity with id of 1 (this is a string, not int)
/api/Entities/1/Action <- Calls a particular action on the entity with the id of 1.
/api/Entities <- Views the entities set.
/api/Entities/Action <- Calls the particular action on the entities set.
The problem I'm encountering is the last one. That is currently being intercepted by the first case, as the id is a string.
Any assistance would be greatly appreciated!
A very good reference is here
Without using Attribute based routing the answer is somewhat long winded as the RPC style (Action) and Rest methods do not co-exist nicely if you are matching to the same verb. As you have noticed the GET() and DOSOMETHING() are seen as duplicate method signatures on the same controller. To get around this you could try use two controllers:
So I would recommend using attribute based routing; however, it is possible with normal methods:
Using standard routing...
Set your routes like so
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "ActionDefaultApi",
routeTemplate: "api/entities/{action}/{actionId}",
defaults: new
{
//Add more as needed
action = "(|dosomething|dosomethingelse)",
actionId = RouteParameter.Optional,
controller = "EntitiesRpc"
});
Controller 1:
public class EntitiesController : ApiController
{
public string Get()
{
return "http://server/api/Entities";
}
public string Get(string id)
{
return string.Format("http://server/api/Entities/{0}", id);
}
}
Controller 2:
public class EntitiesRpcController : ApiController
{
[HttpGet]
public string DoSomething()
{
return "http://server/api/Entities/doSomething";
}
[HttpGet]
public string DoSomething(string actionId)
{
return string.Format("http://server/api/Entities/doSomething/{0}", actionId);
}
[HttpGet]
public string DoSomethingElse()
{
return "http://server/api/Entities/doSomethingelse";
}
[HttpGet]
public string DoSomethingElse(string actionId)
{
return string.Format("http://server/api/Entities/doSomethingelse/{0}", actionId);
}
}
Now assumming you used attribute based routing you could go back to one controller and use something like this:
public class EntitiesController : ApiController
{
[Get("Entities")]
public string Get()
{
return "http://server/api/Entities";
}
[Get("Entities/{id}")]
public string Get(string id)
{
return string.Format("http://server/api/Entities/{0}", id);
}
[Get("Entities/doSomething")]
public string DoSomething()
{
return "http://server/api/Entities/doSomething";
}
[Get("Entities/doSomething/{actionId}")]
public string DoSomething(string actionId)
{
return string.Format("http://server/api/Entities/doSomething/{0}", actionId);
}
[Get("Entities/doSomethingelse")]
public string DoSomethingElse()
{
return "http://server/api/Entities/doSomethingelse";
}
[Get("Entities/doSomethingelse/{actionId}")]
public string DoSomethingElse(string actionId)
{
return string.Format("http://server/api/Entities/doSomethingelse/{0}", actionId);
}
}
Related
I have couple of get methods in my web API class. The route config is as below;
config.Routes.MapHttpRoute(
name: "CustomApi",
routeTemplate: "api/test/{action}/{id}",
defaults: new { controller = "item", action="testBring", id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
The controller class is as below;
public class ItemController : ApiController
{
// GET: api/Item
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
[ActionName("testBring")]
[HttpGet]
public string testBring()
{
return "value";
}
// GET: api/Item/5
public string Get(int id)
{
return "value";
}
// POST: api/Item
[HttpPost]
public void CreateItem([FromBody]string value)
{
}
// PUT: api/Item/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE: api/Item/5
public void Delete(int id)
{
}
}
When I try to call /api/test, testBring method gets called. But when I call /api/Item I get an error that multiple actions were found that match the request ... Why is it so?
/api/test maps fine to the first route template api/test/{action}/{id} because of the default parameters included, which allow it to call a specific action.
However, /api/Item maps to the second route template api/{controller}/{id} and the route table is unable to determine which action you wanted since there are multiple HttpGet actions (ie Get() and testBring() ) that match the requested path.
According to the documentation, if I have this in my WebApiConfig.cs:
config.Routes.MapHttpRoute(
name: "ActionApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I should be able to route to a method in my API controller using a URL like this:
http://localhost:55601/api/Customers/Search
Here is my method:
[ResponseType(typeof(int))]
[HttpPost]
public IHttpActionResult Search([FromBody]CustomerDTO SearchTerm)
{
string Name = SearchTerm.Name;
string Email = SearchTerm.Email;
string PhoneNumber = SearchTerm.Phone;
var customer = db.Customers.Single(c => c.Name == Name && c.EmailAddress == Email && c.PhoneNumber == PhoneNumber);
return Ok(customer.id);
}
I'm sending the search data as a JSON object (using HTTP POST method) in the request body.
However, I get an error saying:
Multiple actions were found that match the request
I only have one method in this controller called Search.
I would have thought this should be pretty straightforward, and work the same way it does with MVC controllers. But I think I'm missing something obvious. Can anyone tell me what it is?
EDIT: As per #KevinLaw's request, adding code for controller showing upblic methods. Also, for further information the following request (HTTP GET) works as expected:
http://localhost:55601/api/Customers?email=[recipient#domain]
public class CustomersController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: api/Customers
public IQueryable<Customer> GetCustomers()
{
//...
}
// GET: api/Customers/5
[ResponseType(typeof(Customer))]
public IHttpActionResult GetCustomer(int id)
{
//...
}
// GET: api/Customers/5
[ResponseType(typeof(Customer))]
public IHttpActionResult GetCustomerByEmail(string email)
{
//...
}
// PUT: api/Customers/5
[ResponseType(typeof(void))]
public IHttpActionResult PutCustomer(int id, Customer customer)
{
//...
}
// POST: api/Customers
[ResponseType(typeof(Customer))]
public IHttpActionResult PostCustomer(Customer customer)
{
//...
}
[ResponseType(typeof(int))]
[HttpPost]
public IHttpActionResult SearchCustomer([FromBody]CustomerDTO SearchTerm)
{
//...
}
// DELETE: api/Customers/5
[ResponseType(typeof(Customer))]
public IHttpActionResult DeleteCustomer(int id)
{
//...
}
}
The problem here is that the WebApiController uses the REST API specs.
Which state that in a Web Api Controller there can be Zero - One Http Verb.
What i mean by that is that you can have one GET,PUT,POST,DELETE,PATCH
The reason you don't have any problem with the GET is because you have them correctly overloaded. () (int) (string).
But your Posts is (Customer) (CustomerDTO) They are both complex objects and the Binder cannot identify which is which when binding to the complex object.
For this to work you need to use Route Attributes or explicit route.
Attribute Routing
Explicit Routing pt1
Explicit Routing pt2
I think the links are enough to get you started.
If you still want to see some code on your specific case leave a comment below and i will give you some examples.
Thanks
EDIT: Added Examples
Attribute Routing
On WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
}
On Controller
[RoutePrefix("api/test")]
public class TestingController : ApiController
{
[HttpGet]
[Route("")]
public IEnumerable<string> Get()
{
return new[] { "value1", "value2" };
}
[HttpPost]
[Route("search")]
public IHttpActionResult Post([FromBody]SearchCriteria criteria)
{
return Ok(criteria);
}
}
public class SearchCriteria
{
public string Name { get; set; }
}
Explicit Routing
On WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes config.Routes.MapHttpRoute(
name: "SearchTest",
routeTemplate: "api/test/search",
defaults: new { controller = "Testing", action = "Search" }
);
config.Routes.MapHttpRoute(
name: "TestingController",
routeTemplate: "api/test/{id}",
defaults: new { controller = "Testing", id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
On Controller
public class TestingController : ApiController
{
[HttpGet]
public IEnumerable<string> Get()
{
return new[] { "value1", "value2" };
}
[HttpPost]
public IHttpActionResult Search([FromBody]SearchCriteria criteria)
{
return Ok(criteria);
}
}
public class SearchCriteria
{
public string Name { get; set; }
}
Hej,
I am unable to post data to the action method through querystring to the action method which is located in the controller class below is my code.
I type a url "http://localhost:53459/api/esb/post/test" to post value and nothing happens
Any help would be appreciated.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{value}",
defaults: new { value = RouteParameter.Optional }
);
}
}
[RoutePrefix("api/esb")]
public class EsbController : ApiController
{
[Route("get")]
[HttpGet]
public string Get()
{
return "Hello there!";
}
[Route("post")]
[HttpPost]
[AcceptVerbs("POST")]
public string Post([FromUri]string value)
{
return string.Format("{0} is posted successfully ", value);
}
[Route("put")]
[HttpPut]
public string Put([FromUri] string value)
{
return string.Format("{0} is updated successfully ", value);
}
[Route("delete")]
[HttpDelete]
public string Delete(string value)
{
return string.Format("{0} is deleted successfully ", value);
}
}
If you are typing the url into a browser you are constructing a GET request so it will never reach your Post action. You can confirm this by adding "GET" to the allowed verbs on the action (note: remove the [HttpPost] attribute).
[Route("post")]
[AcceptVerbs("POST", "GET")]
public string Post([FromUri]string value)
{
return string.Format("{0} is posted successfully ", value);
}
remove parameter binding [FromUri] and update the Route like
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{value}",
defaults: new { value = RouteParameter.Optional }
);
}
}
Controller
[RoutePrefix("api/esb")]
public class EsbController : ApiController
{
[Route("get")]
[HttpGet]
public string Get()
{
return "Hello there!";
}
[Route("post/{value}")]
[HttpPost]
[AcceptVerbs("POST")]
public string Post(string value)
{
return string.Format("{0} is posted successfully ", value);
}
}
This is working for me, try to use Postman in chrome or fiddler.
POST http://localhost:XXXXX/api/esb/post/test HTTP/1.1
For dot.Net Core (I'm using v2.0), use FromRoute
[AllowAnonymous]
[HttpPost]
[Route("validateEmail/{validationCode}")]
public async Task<IActionResult> ValidateEmail(
[FromRoute] Guid validationCode)
{
await authService.ValidateEmailAsync(validationCode);
return Ok();
}
Then post like this:
public class EmployeesController : ApiController
{
public IEnumerable<Employee> GetAllEmployees()
{
return (new EmployeeData()).GetEmployeeRecords();
}
public Employee GetSingleEmployee(int id)
{
return (new EmployeeData()).GetSingleEmployeeRecord(id);
}
public void Delete(int id)
{
(new EmployeeData()).DeleteEmployeeRecord(id);
}
}
This is what I have now. I'm trying to make the app call the first function with a get call api/employee and the second one with a get call api/employee/(an ID number) eg api/employee/75. The get call always goes to the first one. How do I solve this?
This is my routing:
namespace EmployeeApp
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { action = "Index", id=UrlParameter.Optional }
);
}
}
}
After a brief discussion in chat, we discovered that the issue here is that you have two different route configuration files, one for MVC and one for WebAPI. While you could change the name of your actions to match the convention of the WebAPI route configuration (i.e. change GetSingleEmployee to GetEmployeeById), there is another way which would allow you to keep the actions named the same, and have more control over your desired routes. My recommendation is to use the newer Attribute Routing syntax.
First, change your configuration class like so:
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
// Other Web API configuration not shown.
}
next, change your class signature to define the routes through attributes:
[RoutePrefix("api/employee")]
public class EmployeesController : ApiController
{
// http://example.com/api/employee/
[Route("")]
public IEnumerable<Employee> GetAllEmployees()
{
return (new EmployeeData()).GetEmployeeRecords();
}
// http://example.com/api/employee/75
[Route("{id:int}"]
public Employee GetSingleEmployee(int id)
{
return (new EmployeeData()).GetSingleEmployeeRecord(id);
}
// http://example.com/api/employee/Delete/75
[Route("Delete/{id:int}")]
public void Delete(int id)
{
(new EmployeeData()).DeleteEmployeeRecord(id);
}
}
This controller now clearly shows it's routes, rather than the routes being defined somewhere else, or being defined by an obscure name matching convention.
Is there a way to perform parameter type based overloading of an Action method ?
i.e. Is it possible to do the following in a Controller
public class MyController : ApiController
{
public Foo Get(int id) { //whatever }
public Foo Get(string id) { //whatever }
public Foo Get(Guid id) { //whatever }
}
If so, what changes need to be made to the Route table.
This kind of scenario is not well supported by the standard routing methods.
You may want to use attribute based routing instead as this gives you a lot more flexibility.
Specifically look at the route constraints where you can route by the type:
// Type constraints
[GET("Int/{x:int}")]
[GET("Guid/{x:guid}")]
Anything else will turn into a bit of a hack... e.g.
If you did attempt it using standard routing you would probably need to route to the correct action via it's name, then use reg ex's constraints (e.g. guid) to route to the required default action.
Controllers:
public class MyController : ApiController
{
[ActionName("GetById")]
public Foo Get(int id) { //whatever }
[ActionName("GetByString")]
public Foo Get(string id) { //whatever }
[ActionName("GetByGUID")]
public Foo Get(Guid id) { //whatever }
}
Routes:
//Should match /api/My/1
config.Routes.MapHttpRoute(
name: "DefaultDigitApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetById" },
constraints: new { id = #"^\d+$" } // id must be digits
);
//Should match /api/My/3ead6bea-4a0a-42ae-a009-853e2243cfa3
config.Routes.MapHttpRoute(
name: "DefaultGuidApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetByGUID" },
constraints: new { id = #"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$" } // id must be guid
);
//Should match /api/My/everything else
config.Routes.MapHttpRoute(
name: "DefaultStringApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetByString" }
);
Updated
I would normally use a POST if doing a FromBody (perhaps use the FromUri with the model instead) but your requirements could be met by adding the following.
For the controller
[ActionName("GetAll")]
public string Get([FromBody]MyFooSearch model)
{
if (model != null)
{
//search criteria at api/my
}
//default for api/my
}
//should match /api/my
config.Routes.MapHttpRoute(
name: "DefaultCollection",
routeTemplate: "api/{controller}",
defaults: new { action = "GetAll" }
);
You can code your methods as below
[Route("api/My/{id:int}")]
public string Get(int id)
{
return "value";
}
[Route("api/My/{name:alpha}")]
public string Get(string name)
{
return name;
}
[Route("api/My/{id:Guid}")]
public string Get(Guid id)
{
return "value";
}