0

I'm working on a web API, based on MVC4 RC and using database-first Entity Framework as my model. 2 of the entities I have are Item and Group. There's a many-to-many relationship between these 2 entities.

Now, after quite easily implementing the API of CRUD operation for both, using the standard HTTP methods (GET, POST, PUT and DELETE), I came to the point in which I want to implement the binding and unbinding of items to and from groups.

I've tries other verbs, such as LOCK and UNLOCK, without success (they seem not to support them), and tried to somehow manipulate the POST and the PUT commands, again, without success.

Does any of you good people have an idea how to implement this?

Thanks a lot!

nir.arazi
  • 63
  • 10

1 Answers1

1

You can represent the many-to-many as a sub-collection on the root resource. E.g. You have /items/1234 and /groups/4567 - you could have groups as a subcollection as /items/1234/groups or /groups/4567/items

Either way is equally valid. I usually go the route of using a PUT to set the relationship and a DELETE to remove it - some would say that's not really REST but it's worked fine in the scenarios I've used it in.

PUT /items/1234/groups/4567 - create a relationship between item 1234 and group 4567 DELETE /items/1234/groups/4567 - delete a relationship between item 1234 and group 4567

This post helped me a lot. When I was looking into this last...

How to handle many-to-many relationships in a RESTful API?

Update: Routing

So for these more complex scenarios we've ended up simply using more specific routes. It can get ugly quickly trying to cram everything into a single generic route. We've got a suite of unit tests that make sure the relevant URL gets routed to the right controller and action.

    // routes
    routes.MapHttpRoute(
        name: "items.groups",
        routeTemplate: "items/{itemId}/groups/{groupId}",
        defaults: new { controller = "ItemGroup", groupId = RouteParameter.Optional });

The ItemGroupController then has Get, Delete and Put methods. Which we unit test like this...

    // unit tests
    [Test]
    public void PutItemGroup()
    {
        RoutingResult routingResult = this.GenerateRoutingResult(HttpMethod.Put, "~/items/1234/groups/4567");
        Assert.IsNotNull(routingResult);
        Assert.AreEqual("ItemGroup", routingResult.Controller);
        Assert.AreEqual("Put", routingResult.Action);
        Assert.AreEqual("1234", routingResult.RouteData.Values["itemId"]);
        Assert.AreEqual("4567", routingResult.RouteData.Values["groupId"]);
    }

    [Test]
    public void GetItemGroups()
    {
        RoutingResult routingResult = this.GenerateRoutingResult(HttpMethod.Get, "~/items/1234/groups");
        Assert.IsNotNull(routingResult);
        Assert.AreEqual("ItemGroup", routingResult.Controller);
        Assert.AreEqual("GetAll", routingResult.Action);
        Assert.AreEqual("1234", routingResult.RouteData.Values["itemId"]);
    }

    [Test]
    public void GetItemGroup()
    {
        RoutingResult routingResult = this.GenerateRoutingResult(HttpMethod.Get, "~/items/1234/groups/4567");
        Assert.IsNotNull(routingResult);
        Assert.AreEqual("ItemGroup", routingResult.Controller);
        Assert.AreEqual("Get", routingResult.Action);
        Assert.AreEqual("1234", routingResult.RouteData.Values["itemId"]);
        Assert.AreEqual("4567", routingResult.RouteData.Values["groupId"]);
    }

    [Test]
    public void DeleteItemGroup()
    {
        RoutingResult routingResult = this.GenerateRoutingResult(HttpMethod.Delete, "~/items/1234/groups/4567");
        Assert.IsNotNull(routingResult);
        Assert.AreEqual("ItemGroup", routingResult.Controller);
        Assert.AreEqual("Delete", routingResult.Action);
        Assert.AreEqual("1234", routingResult.RouteData.Values["itemId"]);
        Assert.AreEqual("4567", routingResult.RouteData.Values["groupId"]);
    }

    private RoutingResult GenerateRoutingResult(HttpMethod method, string relativeUrl)
    {
        HttpConfiguration httpConfiguration = new HttpConfiguration(this.HttpRoutes);
        HttpRequestMessage request = new HttpRequestMessage(method, string.Format("http://test.local/{0}", relativeUrl.Replace("~/", string.Empty)));
        IHttpRouteData routeData = this.HttpRoutes.GetRouteData(request);

        Assert.IsNotNull(routeData, "Could not locate route for {0}", relativeUrl);

        this.RemoveOptionalRoutingParameters(routeData.Values);

        request.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
        request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, httpConfiguration);

        IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(httpConfiguration);
        HttpControllerContext controllerContext = new HttpControllerContext(httpConfiguration, routeData, request)
            {
                ControllerDescriptor = controllerSelector.SelectController(request)
            };

        HttpActionDescriptor actionDescriptor = controllerContext.ControllerDescriptor.HttpActionSelector.SelectAction(controllerContext);
        if (actionDescriptor == null)
        {
            return null;
        }

        return new RoutingResult
            {
                Action = actionDescriptor.ActionName,
                Controller = actionDescriptor.ControllerDescriptor.ControllerName,
                RouteData = routeData
            };
    }

    private void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValueDictionary)
    {
        int count = routeValueDictionary.Count;
        int index1 = 0;
        string[] strArray = new string[count];
        foreach (KeyValuePair<string, object> keyValuePair in routeValueDictionary)
        {
            if (keyValuePair.Value == RouteParameter.Optional)
            {
                strArray[index1] = keyValuePair.Key;
                ++index1;
            }
        }

        for (int index2 = 0; index2 < index1; ++index2)
        {
            string key = strArray[index2];
            routeValueDictionary.Remove(key);
        }
    }

    private class RoutingResult
    {
        public string Controller { get; set; }

        public string Action { get; set; }

        public IHttpRouteData RouteData { get; set; }
    }

Cheers, Dean

Community
  • 1
  • 1
Dean Ward
  • 4,793
  • 1
  • 29
  • 36
  • Thanks Dean! I'm now having problems with the routing rule. I've used /api/{controller}/{id}/{action}/{id2} with id, action and id2 being optional, with the intention that a PUT verb on /api/groups/1/items/2 will route to my bind method, and a DELETE one on the same one will route to the unbind method. Can't seem to get this working. Any clue on that? – nir.arazi Jul 15 '12 at 14:33
  • Haha, been having a lot of fun with that recently; making a general rule for every case can be *painful*! We've ended up breaking ours up into a number of rules that deal with specific cases like this. In a way the attribute-based routing in WCF Web Api (the predecessor to the current framework) made things like this flow better. I'll edit the response to add some example routing we ended up using... – Dean Ward Jul 15 '12 at 14:41