Batching has been supported in OData for some time. In WCF Data Services, batching was largely a black box. The OData support for Web API opens up the batching pipeline, but you are still limited to batched queries and change sets out-of-the-box. What if you wanted to allow batched actions as well?
As it turns out the DefaultODataBatchHandler will only accept the POST method when the request is for a change set. Since an OData action could have side effects, they are required to be requested via the POST method. This clearly causes a problem if we want to support batching and actions. What can we do about it?
Extending the Default OData Batch Handler
HTTP batching has become an intrinsic capability in ASP.NET Web API and OData. Altering or otherwise extending the default behavior is now very straight forward – almost.
While it's true that we can easily subclass DefaultODataBatchHandler and override its behaviors, we are not able to change how the OData APIs parse the batch request. As a result, we are not able to influence the batch parsing process - or are we?
One solution would be to completely supplant the default OData parsing routines and replace them with our own. This approach is highly undesirable because we don't want to change any of the inherent query or change set behaviors. What other options do we have?
Applying Pipes and Filters
A batch request is nothing more than a multipart MIME encoded document that contains nested HTTP requests. All we need to do is evaluate each nested request, determine if it represents an OData action, and filter those requests from the requests that you would normally provide to the OData parsing routines. For each action we identify, we need to create and return an OperationRequestItem from our batch handler. For all other requests, we'll just let them flow through to the base implementation and combine the returned items with our action items.
There's just one caveat that we've overlooked. The OData specification has stringent rules about how batch requests are to be processed. Specifically, it states that requests must be processed in the order that they are received. This requirement doesn't derail our solution, but now we need to ensure that the items we create for actions are merged with the normally created items in the same order as they appear in the request.
The following diagram provides a visualization of the process:
Identifying an OData Action Request
We now have a workable design to support actions in a batch request, but how do we identify that an item in the batch represents an action? The answer is surprisingly easier than we might think. There are three criteria we can use to concretely identify an OData action:
- The request is a POST
- The request is not nested multipart MIME content (e.g. a change set)
- The request URL matches an ActionPathSegment
The first two criteria are easy to implement using the ReadAsHttpRequestMessageAsync and IsMimeMultipartContent extension methods provided by Web API.
We'll satisfy the third criterion by creating our own extension method – IsODataAction. In order to determine whether the nested request is an action, we'll use the path handler associated with batch request to parse the OData path. If the path parses successfully and the last segment is an ActionPathSegment, then we know we have a match.
Custom Batch Handler Landmines
While poking around in the source code for the DefaultODataBatchHandler, I realized that the OData batch processing does not use the Web API libraries to parse the request. This really isn't a big deal and in all likelihood the code is the same as it is in WCF Data Services. The surprise and initial frustration to me was the fickleness of the HttpRequestMessage class when you try to read it from HttpContent using the ReadAsHttpRequestMessageAsync.
First, the HttpRequestMessage class enforces the HTTP 1.1 requirement for the Host header. Under most scenarios this would be completely reasonable; however, in a batch request the client must already provide the Host header in the root request, which makes it unlikely or easy to forget to specify in the nested requests. To be tolerant of this potential issue, I had to programmatically inject the Host header from the main batch request to each child request before it is deserialized – if it's not already present.
The second issue is similar in nature. The URL specified for each of the nested requests in the batch will almost certainly be relative. The examples for batch requests you can find online vary, where some show the relative URL with a leading '/' and others do not. It turns out that the relative URL will fail to build properly using ReadAsHttpRequestMessageAsync if it does not have the leading '/'. If you examine the implementation of the private CreateRequestUri method within HttpContentMessageExtensions, you'll see that it fails because the URL is constructed as an absolute URL using String.Format. I'm not quite sure I would qualify this as a bug, but at a minimum the code should be more tolerant of this behavior or use Uri to combine the URLs. A comment in the source code does mention why it doesn't use UriBuilder, but it doesn't explain why it doesn't use Uri.
Ultimately, these were not huge issues to overcome, but they were certainly unexpected. I could have written all my own parsing as needed (and in some cases I still had to), but my objective was to make the Web API libraries do as much as that work for me as possible.
Putting It All Together
One of the greatest strengths of how batching is supported in ASP.NET Web API is that it is completely orthogonal to everything else. Why should a controller know it's executing something from a batched request? It is a cross-cutting concern after all. To enable OData batching in your application, you only need to add the following Web API configuration:
This will light up a new endpoint at [route prefix]/$batch, which is the out-of-the-box route mapping for OData. A batch request can be made to any entity controller operation and, now, to any action. Also keep in mind that batch requests do not need to target the same controller. This means a client can issue a query, change set, or execute an action against any combination of services!
I've excluded much of the HTML page for brevity, but the following demonstrates how to compose an OData action and query in a batch request and then display the results in the page. The complete example is included in the sample code.
Although we had to jump through a few hoops, it is possible to support OData batching with actions. Hopefully, you'll find this useful in expanding batching support in your applications. As always, the full source code is provided with all of the relevant unit tests. The solution was built with Visual Studio .NET 2013, .NET 4.5.1, ASP.NET MVC 5.0, and ASP.NET Web API 2.0 for OData.