Creating a RESTful Web Service Using ASP.Net MVC Part 7 – A More Complex Resource

November 4, 2008 01:06 by admin

In this series of posts we have been using a simple GUIDs resource to build a basic framework for a RESTful web service. As we start to explore the other HTTP Actions we need a more complex resource. I decided to create a Products resource that supports the listing, creation, updating and deletion of products. It will also support “child” representations for each category of product.

 

The Products Resource

This table lists the operations I would like the Products resource to support:

Operation HTTP Action
1 List all products GET /products
2 Create a new product POST /products
3 View any product GET /products/{id}
4 Update any product PUT /products/{id}
5 Delete any product DELETE /products/{id}
6 List all products in a category GET /products/{category}
7 Create a new product in a category POST /products/{category}
8 View product in a category GET /products/{category}/{id}
9 Update product in a category PUT /products/{category}/{id}
10 Delete product in a category DELETE /products/{category}/{id}

In an ideal world, all these operations would be handled within a single controller, by different methods. However, with the framework we have developed so far, this is not so easy…

 

Some Background

In the Beta version of MVC there is a class called ActionMethodSelector which is responsible for choosing which method on a controller should handle an action. It works roughly like this:

  1. ActionMethodSelector inspects the controller to find all the methods whose name matches the action or have a matching ActionNameAttribute.
  2. From those methods it then builds two lists:
    • the first list contains methods that have an attribute derived from ActionMethodSelectorAttribute attached and that attribute’s IsValidForRequest method returns true;
    • the second list contains methods that don't have a ActionMethodSelectorAttribute at all.
  3. If the first list contains any methods, this become the "ambiguous" list of methods. If the first list is empty, the second list becomes the "ambiguous" list. In my previous post we used this feature to create an AllOtherVerbs method in the BaseController.
  4. Finally the "ambiguous" list is inspected. If it is empty the HandleUnknownAction method is called. If it contains a single method, the action can proceed. If it contains more than one method, an InvalidOperationException is thrown with an explanation similar to this:

    "The current request for action 'HttpVerb' on controller type 'ProductsController' is ambiguous between the following action methods: …"

Out of the box MVC provides two attribute classes derived from ActionMethodSelectorAttribute:

  • AcceptVerbs – returns true from IsValidForRequest if the HttpMethod of the request matches one of those provided during initialisation of the attribute. This attribute was introduced in my last post.
  • NonAction – always returns false from IsValidForRequest so the method will never be used to handle an action.

 

The Problem

Normally in an MVC project we would be able to create a single controller with separate methods for each of the above operations by using a different action for each. However, in my last post we settled on using the same action “HttpVerb” for all requests. This allowed us to catch unsupported methods, but makes supporting the above operations difficult.

So how can we distinguish the different operations? By using AcceptVerbs on the methods we can distinguish between operations that use the different HTML verbs (i.e. GET, POST, PUT and DELETE). But what about distinguishing between operations using the same HTML verb (e.g. 1, 3, 6 and 8 all use GET)? The only difference between those operations is the number of parameters that can be extracted from the URI. Unfortunately, MVC will not use the parameters to find a best match method on the controller. The logic to do this would be convoluted, probably not work for all cases and significantly increase response times.

 

The Solution(s)

I thought of four possible solutions:

  1. Have two controllers, one that deals with lists of Products and one that deals with a single Product. This requires the use of HttpMethodConstraint in a route to ensure the POST actions (operations 2 and 7) were routed to the “ProductController” rather than the “ProductsController” (the other verbs GET, DELETE and PUT could be routed by the presence of an Id in the URI);
  2. Implement an ActionMethodSelectorAttribute which provides some basic "best match" processing. For instance, in the IsValidForRequest method it could check the number of parameters on the target method matches the number of non-null parameters available from the URI;
  3. Return to using more than one action. This would then require a new approach to catching unsupported methods. We could either:
    • Create a CatchAllActionName attribute derived from ActionNameSelectorAttribute which always returned true from its IsValidName method. So long as the method being decorated with CatchAllActionName did not have any ActionMethodSelectorAttributes it would always be the default action.
    • Accept that in our final web service all actions will be hardcoded into the route information therefore the only reason (in production) that the HandleUnknownAction should be called is if the Http verb used in the request is unsupported;
  4. Handle requests for both lists and single products within the same method. Custom logic within the method would decide which operation was being requested.

The first option did not appeal as it would require yet more routes to be added to the route table. It also seemed to lack the cohesiveness of a single controller that dealt with all product related requests.

Option two felt like a brittle work around. Creating a comprehensive solution to find the best match method would be difficult, especially within an ActionMethodSelectorAttribute which does not have easy access to the other potential matches. Performance would probably take a hit too!

The third option removes our reliance on a well known action (currently “HttpVerb”). Of the two sub options, the first would work, but the second is already supported in the framework. Whichever approach we take, we will have to distinguish between an action that couldn’t be resolved because there was no suitable method and an action that failed because the HTTP verb was not supported. This approach would require more routes than option 4.

The fourth option should be performant but will require similar logic to be implemented throughout the web service. Refactoring could help reduce the amount of code that needs writing.

So which option did I choose? Originally I implemented option 4, but as I wrote up this posting I came up with option 3. In my original post I warned I may make the odd U-Turn along the way… and this is one.

Option 3 will only work if the action is hardcoded into the route, not extracted from the URI. In a truly RESTful web service, the URI should only contain “scoping” information and never “method” information. The scoping information in the URI defines which resource is being acted upon, the HTTP verb dictates what action should be performed (i.e. it provides the Uniform Interface). In RESTful Web Services Richardson and Ruby describe web services that contain method information in the URI as “REST-RPC Hybrid Services”. So if our web service is going to be truly RESTful it will never extract action information from the URI, the action will always be hardcoded. Of the two sub options I choose to go with using the HandleUnknownAction approach as it is already available.

 

Implementing Products

Some of the basic things I did to support the Products resource included:

  • Added a ProductsController, a few Model classes and a Views\Products folder to hold the ASPX and XSLT files. The Model classes maintain an in memory list of products and provide methods for retrieving those products. There are two variants of the ASPX and XSLT files: List and Item;
  • Removed the AllOtherVerbs method from BaseController and updated HandleUnknownAction to return a 405 Method Not Allowed;
  • Removed the HttpVerb action throughout the web service and replaced it with more appropriate action names (e.g. List and Item).

Within the ProductsController the AllowedVerbs property had to be made dynamic. Rather than return a static string (as the GuidController does) it inspects the CurrentContext to determine if an Id has been supplied and changes the “Allowed Verbs” accordingly:

protected override string AllowedVerbs

{

    get

    {

        string verbs = "GET";

 

        // Extra verbs are allowed depending on whether an id is supplied or not

        if (string.IsNullOrEmpty((string)ControllerContext.RouteData.Values["idString"]))

        {

            verbs += ", POST";

        }

        else

        {

            verbs += ", PUT, DELETE";

        }

 

        return verbs;

    }

}

The initialisation of routes had to be altered:

public static void RegisterRoutes(RouteCollection routes)

{

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

 

    routes.MapRoute(

        "ProductsItem",

        "Products/{idString}",

        new { controller = "Products", action = "Item", categoryString = string.Empty },

        new { idString = @"^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$" }

        );

 

    routes.MapRoute(

        "ProductsCategoryItem",

        "Products/{categoryString}/{idString}",

        new { controller = "Products", action = "Item", categoryString = string.Empty, idString = string.Empty },

        new { idString = @"^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$" }

        );

 

    routes.MapRoute(

        "ProductsCategory",

        "Products/{categoryString}",

        new { controller = "Products", action = "List", categoryString = string.Empty, idString = string.Empty }

        );

 

    routes.MapRoute(

        "Default",

        "{controller}",

        new { controller = "Home", action = "List"}

        );

 

    routes.MapRoute(

        "CatchAll",

        "{*url}",

        new { controller = "Error", action = "Http404" }

        );

}

The first MapRoute looks for a GUID immediately after Products in the URI. This route should pick up operations 3, 4 and 5 from the table above. The second MapRoute looks for a GUID after the category so picks up operations 8, 9, 10. The third MapRoute matches a product and category so will pick up operations 6 and 7. Operations 1 and 2 will be picked up by the fourth MapRoute. The fifth route is the catch all introduced in an earlier post.

The code for this version can be downloaded: RESTfulMVCWebService05.zip (43.25 kb). Please let me know what you think. In my next post I hope to finally get around to supporting HTTP verbs other than GET!

 

A Couple of Asides

As I worked on this entry I realised that the result of a POST was often the cached version of a previous GET. The HTTP specification seems to imply this shouldn’t be happening but as Mark Nottingham says in his blog, many browsers don’t comply with the specification too well. How to stop this? Either the server can indicate none of its responses are cached or the client code can force the cache to be ignored during POST requests. I adapted the JavaScript used in the test harness to force caching to be ignored during a POST request. By setting the header "If-Modified-Since" to "Sat 1 Jan 2000 00:00:00 GMT" I always received the correct response. This is one of a few workarounds I saw online.

Another issue pointed out by Stephan was that the code in the BaseController that adds an “Allow” HTTP Header uses the Response.Headers collection directly… functionality that is not available when using the Visual Studio Development Server. You get a PlatformNotSupportedException with the message: "This operation requires IIS integrated pipeline mode." The workaround is to use Response.AppendHeader method instead.


Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading