4

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});
Frank Rem
  • 3,632
  • 2
  • 25
  • 37
Luke Baulch
  • 3,626
  • 6
  • 36
  • 44

1 Answers1

2

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 });
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Unfortunately, decorating the Guests method with [HttpGet] makes the url api/events/5 GET request route to this Guest method which is not the desired functionality. – Luke Baulch May 31 '13 at 01:32
  • I updated my answer. Does that solve the problem? Because, that keeps all as it is, but teaches API to use POST method for POST with events/5. No more HttpGet attribute on top of Guests – Radim Köhler May 31 '13 at 02:11
  • Actually, that solution gives me the error "The requested resource does not support http method 'GET'" when I use GET api/events/5/guests. Using your idea, I'll update my question to include the working solution. – Luke Baulch May 31 '13 at 02:42
  • Yes, that's because you've missed the HttpGet attribute. Just decorate the Guests with `[HttpGet]` and append similar new route as we did for Post, for Get ("DefaultGet" with action name `Get` and constraint HttpMethod.Get) ... all will work... I do use the same – Radim Köhler May 31 '13 at 03:33