Luke Baulch Luke Baulch - 2 months ago 15
reST (reStructuredText) Question

RESTful Web API with Associations. Is it possible?

I have written a REST service using Web API and after reading sections of this Web API Design from Brian Mulloy, was trying to figure out how I could implement associations with Web API.

Web API Design Extract:


Associations

Resources almost always have relationships to other
resources. What's a simple way to express these relationships in
aWebAPI?

Let's look again at the API we modeled in nouns are good,
verbs are bad -theAPI that interacts with our dogs resource.
Remember, we had two base URLs: /dogs and dogs/1234.

We're using HTTP
verbs to operate on the resources and collections. Our dogs belong to
owners. To get all the dogs belonging to a specific owner, or to
create a new dog for that owner, do a GET or a POST:

GET /owners/5678/dogs

POST /owners/5678/dogs

Now, the relationships can be
complex. Owners have relationships with veterinarians, who have
relationships with dogs, who have relationships with food, and so on.
It's not uncommon to see people string these together making a URL 5
or 6 levels deep. Remember that once you have the primary key for one
level, you usually don't need to include the levels above because
you've already got your specific object. In other words, you shouldn't
need too many cases where a URL is deeper than what we have above
/resource/identifier/resource.


So I tried to add a controller method for the association like follows:

public class EventsController : ApiController
{
// GET api/events
public IEnumerable<Event> Get()
{
// get list code
}

// GET api/events/5
public Event Get(int id)
{
// get code
}

// POST api/events
public void Post([FromBody]Event evnt)
{
// add code
}

// POST api/events/5
public void Post(int id, [FromBody]Event evnt)
{
// update code
}

// DELETE api/events/5
public void Delete(int id)
{
// delete code
}

// GET api/events/5/guests
public IEnumerable<Guest> Guests(int id)
{
// association code
}
}


I also modified my route templates to the following:

config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });


Unfortunately, when I do an update/post of the event resource I now get a HTTP 500 Internal Server Error with a response body stating


Multiple actions were found that match the request


I've tried modifying the route templates in conjunction with adding System.Web.Http.HttpPostAttribute (and other HTTP verbs) as well but to no avail.

Has anyone tried this and got it working? Any help would be appreciated. If it is absolutely not possible to have multiples for an http verb then I guess I'll have to abandon associations with my REST service.

EDIT: SOLUTION

Using Radim Köhler's answer, I was able to get this working. Add the HttpGetAttribute to the Guests method like so:

// GET api/event/5/guests
[HttpGet]
public IEnumerable<Guest> Guests(int id)
{
// association code
}


And added an addition route to cater for the default GET action like follows:

config.Routes.MapHttpRoute("DefaultGet",
"api/{controller}/{id}",
new {action = "Get"},
new {httpMethod = new HttpMethodConstraint(HttpMethod.Get)});
config.Routes.MapHttpRoute("ApiWithAssociations",
"api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional});

Answer

The solution, could be in an explicit POST mapping

Just add new definition, which will be used for events/5 POST

// explicit Post() mapping 
config.Routes.MapHttpRoute(
    name: "DefaultPost",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { action = "Post" }
    , constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
    );

// existing
config.Routes.MapHttpRoute("ApiWithAssociations",
    "api/{controller}/{id}/{action}");
config.Routes.MapHttpRoute("DefaultApi",
    "api/{controller}/{id}",
    new { id = RouteParameter.Optional });