composite key resource REST service

Mitselplik picture Mitselplik · Apr 24, 2013 · Viewed 10k times · Source

I've come across a problem at work where I can't find information on the usual standard or practice for performing CRUD operations in a RESTful web service against a resource whose primary key is a composite of other resource ids. We are using MVC WebApi to create the controllers. For example, we have three tables:

  • Product: PK=ProductId
  • Part: PK=PartId
  • ProductPartAssoc: PK=(ProductId, PartId)

A product can have many parts and a part can be a component of many products. The association table also contains additional information relevant to the association itself than needs to be editable.

We have ProductsController and PartsController classes that handle the usual GET/PUT/POST/DELETE operations using route templates defined as: {controller}/{id}/{action} such that the following IRIs work:

  • GET,POST /api/Products - returns all products, creates a new product
  • GET,PUT,DELETE /api/Products/1 - retrieves/updates/deletes product 1
  • GET,POST /api/Parts - returns all parts, creates a new part
  • GET,PUT,DELETE /api/Parts/2 - retrieves/updates/deletes part 2
  • GET /api/Products/1/Parts - get all parts for product 1
  • GET /api/Parts/2/Products - get all products for which part 2 is a component

Where I am having trouble is in how to define the route template for ProductPartAssoc resources. What should the route template and IRI look like for getting the association data? Adhering to convention, I would expect something like:

  • GET,POST /api/ProductPartAssoc - returns all associations, creates an association
  • GET,PUT,DELETE /api/ProductPartAssoc/[1,2] - retrieves/updates/deletes association between product 1 and part 2

My coworkers find this aesthetically displeasing though and seem to think it would be better to not have a ProductPartAssocController class at all, but rather, add additional methods to the ProductsController to manage the association data:

  • GET,PUT,DELETE /api/Products/1/Parts/2 - get data for the association between product 1 and part 2 rather than data for part 2 as a member of part 1, which would conventionally be the case based on other examples such as /Book/5/Chapter/3 that I have seen elsewhere.
  • POST No clue here what they expect the IRI to look like. Unfortunately, they're the decision makers.

At the end of the day, I guess what I am seeking is either validation, or direction that I can point to and say "See, this is what other people do."

What is the typical practice for dealing with resources identified by composite keys?

Answer

danludwig picture danludwig · Apr 24, 2013

I too like the aesthetics of /api/Products/1/Parts/2. You could also have multiple routes go to the same action, so you could double up and also offer /api/Parts/2/Products/1 as an alternate URL for the same resource.

As for POST, you already know the composite key. So why not eliminate the need for POST and just use PUT for both creation and updates? POST to a collection resource URL is great if your system generates the primary key, but in cases where you have a composite of already known primary keys, why do you need POST?

That said, I also like the idea of having a separate ProductPartAssocController to contain the actions for these URL's. You would have to do a custom route mapping, but if you're using something like AttributeRouting.NET that is very easy to do.

For example we do this for managing users in roles:

PUT, GET, DELETE /api/users/1/roles/2
PUT, GET, DELETE /api/roles/2/users/1

6 URL's, but only 3 actions, all in the GrantsController (we call the gerund between users and roles a "Grant"). Class ends up looking something like this, using AttributeRouting.NET:

[RoutePrefix("api")]
[Authorize(Roles = RoleName.RoleGrantors)]
public class GrantsController : ApiController
{
    [PUT("users/{userId}/roles/{roleId}", ActionPrecedence = 1)]
    [PUT("roles/{roleId}/users/{userId}", ActionPrecedence = 2)]
    public HttpResponseMessage PutInRole(int userId, int roleId)
    {
        ...
    }

    [DELETE("users/{userId}/roles/{roleId}", ActionPrecedence = 1)]
    [DELETE("roles/{roleId}/users/{userId}", ActionPrecedence = 2)]
    public HttpResponseMessage DeleteFromRole(int userId, int roleId)
    {
        ...
    }

    ...etc
}

This seems a fairly intuitive approach to me. Keeping the actions in a separate controller also makes for leaner controllers.