Routing MVC APIs with Multiple Get Actions

Thousands of people seem to be asking the same question about stack overflows, but there seems to be no single solution to this problem. I'm going to ask him again ...

I have an API controller that does the following:

// GET api/Exploitation public HttpResponseMessage Get() { var items = _exploitationRepository.FindAll(); var mappedItems = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(items); var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItems); response.Headers.Location = new Uri(Url.Link("DefaultApi", new { })); return response; } // GET api/Exploitation/5 [HttpGet, ActionName("Get")] public HttpResponseMessage Get(int id) { var item = _exploitationRepository.FindById(id); var mappedItem = Mapper.Map<Exploitation, ExploitationView>(item); var response = Request.CreateResponse<ExploitationView>(HttpStatusCode.OK, mappedItem); response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id })); return response; } // GET api/Exploitation/GetBySongwriterId/5 [HttpGet, ActionName("GetBySongwriterId")] public HttpResponseMessage GetBySongwriterId(int id) { var item = _exploitationRepository.Find(e => e.Song.SongWriterSongs.Any(s => s.SongWriterId == id)) .OrderByDescending(e => e.ReleaseDate); var mappedItem = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(item); var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItem); response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id })); return response; } // GET api/Exploitation/GetBySongwriterId/5 [HttpGet, ActionName("GetBySongId")] public HttpResponseMessage GetBySongId(int id) { var item = _exploitationRepository.Find(e => e.SongId == id) .OrderByDescending(e => e.ReleaseDate); var mappedItem = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(item); var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItem); response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id })); return response; } 

In my APIConfig, I defined the following routes:

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

I find that I can access the following actions without problems: / api / exploitation / api / exploitation / getbysongwriterid / 1 / api / exploitation / getbysongid / 1

When I try to access / api / operation / 1, I get this exception

 "Multiple actions were found that match the request: System.Net.Http.HttpResponseMessage Get(Int32) on type Songistry.API.ExploitationController System.Net.Http.HttpResponseMessage GetBySongwriterId(Int32)" exception. 

Can anyone understand what is wrong with my routes? Or is it wrong with something else?

+4
source share
3 answers

I found an elegant solution to the problem.

I changed my ApiRouteConfig to the following routes:

  config.Routes.MapHttpRoute( name: "DefaultGetApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional, action = "Get" }, constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Get) } ); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional }, constraints: new { id = @"\d+" } ); config.Routes.MapHttpRoute( name: "ActionApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional } ); 

Now I can access:

 /api/exploitation /api/exploitation/1 /api/exploitation/getbysongid/1 /api/exploitation/getbysongwriterid/1 

I did not need to change my actions with the controller to work with this new routing configuration.

If you had several PUT or POST actions, you could create new routes that would look like this:

  config.Routes.MapHttpRoute( name: "DefaultGetApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional, action = "Put" }, constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Put) } ); config.Routes.MapHttpRoute( name: "DefaultGetApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional, action = "Delete" }, constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Delete) } ); 

I hope this answer helps everyone, as this seems to be a common problem that people are facing.

+6
source

The problem is that / api / operating / 1 falls under:

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

All your GET methods satisfy this routing, and especially because {id} is optional and their controller is the same.

So, you have one HTTP GET request from the client and several methods that accept GET requests. He does not know who to go to.

 api/{controller}/{action}/{id} //This works fine because you specified which action explicitly 

Hope the answer to your question.

0
source

Try the following in your route definition. Keep only the following route:

  config.Routes.MapHttpRoute( name: "ActionApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional, action = "Get" }, constraints: new { id = @"\d+" } ); 

Make the first Get method private, change the second so that the id value has a default value:

 // GET api/Exploitation private HttpResponseMessage Get() { // implementation stays the same but now it private } // GET api/Exploitation/5 [HttpGet, ActionName("Get")] public HttpResponseMessage Get(int id = 0) { if (id == 0) { return Get(); } // continue standard implementation } 

This method (I have not tested it myself) I expect that:

  • api / Exploitation / will display api / Exploitation / Get (as the default action) with id = 0 by default param
  • api / Exploitation / 1 will be displayed in api / Exploitation / Get / 1, so it will call Get (1)
  • api / Operation / someOtherAction / 345 will call the correct action method

It might work. A more rigid route definition may be:

  config.Routes.MapHttpRoute( name: "ApiWithRequiredId", routeTemplate: "api/{controller}/{action}/{id}", defaults: null /* make sure we have explicit action and id */, constraints: new { id = @"\d+" } ); config.Routes.MapHttpRoute( name: "ApiWithOptionalId", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional, action = "Get" }, constraints: new { action = "Get" /* only allow Get method to work with an optional id */, id = @"\d+" } ); 

But something like that ... try it, I hope it solves your problem.

0
source

All Articles