In my last post I explained briefly how batching works with a REST API and showed you a sample application with the Fiddler trace of how batching can help speed up your application. In this post I want to talk more nuts-and-bolts and explain exactly how batches work with OData & SharePoint. As of this writing, I’m talking specifically about the OData v3.0 specification, but as far as batching is concerned, it’s basically the same in OData v4.0… there are a few improvements though which are listed in this link in the OData v4.0 specification.
There are two main parts of a batch request as defined in the spec. The first part, the batch header describes the batch being submitted. The second part, the batch body, is the work that will be performed as part of the batch.
Understanding the OData Batch Request Header
The concept of a batch request in OData is quite simple. In non-batch requests you issue an HTTP request that includes a few things about the request like the HTTP verb, the endpoint & the protocol. You then include a series of HTTP header values that describe the data & how you want to get the response back. Finally if you are creating or updating something, you send data along. So a typical request looks like this:
POST http://[your-sharepoint-site]/_api/$batch HTTP/1.1
Host: [your-sharepoint-site-domain]
X-RequestDigest: [your-sharepoint-digest]
Content-Type: multipart/mixed; boundary=batch_36522ad7-fc75-4b56-8c71-56071383e77b
[batch-request-body]
A batch request differs in that the first part of the request describes the batch. It must conform to a few rules, for instance:
- It MUST use the endpoint
$batch
- It MUST use a HTTP header
Content-Type
ofmultipart/mixed
and it must include a batch boundary (more on that in a moment)
The rest of the request contains the actual batch request body. This will include all the requests you are sending to the server. Look at the snippet above again. Take notice of the Content-Type
in the request above. Towards the end you see something called a boundary. This is used to define the start & stop of a batch request. This value doesn’t have to be a GUID, but that’s what seems to be commonly used.
So the introduction to the batch request that you see above is fairly straightforward. Now we want to look at the actual batch body.
Understanding the OData Batch Request Body
The batch body is broken up into three pieces. There’s the prefix, the actual body of requests & the suffix. First let me focus on the prefix & suffix because they are the easiest to cover. These define the start & stop parts of the batch. One thing that’s a little strange is that you can actually have multiple prefixes in the batch request but only one suffix. Huh? Let me explain…
A batch request can contain multiple changesets. A changeset is something that contains one or more write operations or non HTTP GET requests. Each changeset needs a prefix to define some common HTTP header values. That’s all you need to know for now… I’ll dive into some aspects of changesets more in the section Digging Deeper into OData Batch Changesets below.
So, let’s say you want to issue a series of requests in a batch like:
- get all items
- create two items
- get all items
This is four requests that can be batched up. You have two HTTP GET requests that will surround a changeset that creates two items. After the batch header, you need to start the batch using line that looks like this: —batch_[batch-boundary-id]
. That boundary is coming from the header. Immediately after this line you MUST include the following lines when executing an HTTP GET:
Content-Type: application/http
Content-Transfer-Encoding: binary
These describe this part of the batch. After adding a blank line you add your actual request and include any necessary headers. For instance here’s what that part of the request would look like:
--batch_45473b4e-5ce5-409d-dd5a-3d60082df906
Content-Type: application/http
Content-Transfer-Encoding: binary
GET https://[your-sharepoint-site]/_api/web/lists/getbytitle('Drivers')/items?$orderby=Title HTTP/1.1
Host: [your-sponline-domain]
Accept: application/json;odata=verbose
Notice I started the request with the HTTP verb I wanted to perform (GET) followed by the actual query endpoint and finished it off with the protocol to use (HTTP/1.1). This should be familiar because this is what you do with a non-batch request in OData.
What about creating, updating or deleting entities in the batch? Again, that’s where changesets come into play…
Digging Deeper into OData Batch Changesets
Similar to issuing an HTTP GET query in a batch, a changeset must start with the batch boundary and include some specific lines describing the actual changeset. the content type & transfer encoding is different for a changeset than query request as you can see here:
--batch_45473b4e-5ce5-409d-dd5a-3d60082df906
Content-Type: multipart/mixed; boundary="changeset_f9c96a07-641a-4897-90ed-d285d2dbfc2e"
Host: [your-sponline-domain]
Content-Length: [size in bytes of the changeset]
Content-Transfer-Encoding: binary
Let’s pick this part. First you start the batch again with the same first line. The next line says that the Content-Type
must be multipart/mixed and also define a boundary for the changeset. Similar to the definition of the batch in the batch header, this is also going to be something unique as you can see from that snippet. Skip ahead to the Content-Length
line… that is the total size, in bytes, of the actual changeset you are sending across. Finally, the Content-Transfer-Encoding
must be set to binary.
Now we can define the changeset. Just like the batch, we have to start the changeset and define some headers for the request. Each request within a changeset should include an opening header. Here’s what creating two items would look like:
--changeset_f9c96a07-641a-4897-90ed-d285d2dbfc2e
Content-Type: application/http
Content-Transfer-Encoding: binary
POST https://[your-sharepoint-site]/_api/web/lists/getbytitle('Drivers')/items HTTP/1.1
Content-Type: application/json;odata=verbose
{"__metadata":{"type":"SP.Data.DriversListItem"},"Title":"Fernando Alonso","Team":"Ferrari"}
--changeset_f9c96a07-641a-4897-90ed-d285d2dbfc2e
Content-Type: application/http
Content-Transfer-Encoding: binary
POST https://[your-sharepoint-site]/_api/web/lists/getbytitle('Drivers')/items HTTP/1.1
Content-Type: application/json;odata=verbose
{"__metadata":{"type":"SP.Data.DriversListItem"},"Title":"Filipe Massa","Team":"Ferrari"}
--changeset_f9c96a07-641a-4897-90ed-d285d2dbfc2e--
There are a few things to take note of on this one. First, notice that even though we have two different requests, each one is prefixed by the changeset declaration with two common header values. Notice how the Content-Type
and Content-Transfer-Encoding
are set to the same two specific values. Then, after a space, you see the actual HTTP POST request to create an item. Again, this should be familiar as it is what a typical non-batch REST request looks like. It’s important to make sure you have the blank lines in there as that’s part of the HTTP/1.1 protocol. Lastly notice the last line is indicating the end of the changeset by putting the changeset ID in again but adding two dashes to the end of the ID.
That’s basically it! The last thing you need to do to indicate the batch is complete is, as the last line, add the batch ID but include two dashes at the end like this: --batch_45473b4e-5ce5-409d-dd5a-3d60082df906--
.
Put it all together and you get a request that looks like this:
--batch_45473b4e-5ce5-409d-dd5a-3d60082df906
Content-Type: multipart/mixed; boundary="changeset_f9c96a07-641a-4897-90ed-d285d2dbfc2e"
Host: [your-sponline-domain]
Content-Length: [size in bytes of the changeset]
Content-Transfer-Encoding: binary
Ordering Requests Within OData Batches
You might be wondering “what about ordering requests?” Good question. There are two things to keep in mind about ordering responses. First, the order of queries and changesets within a batch are executed in the order they are specified. However, the order of requests WITHIN a changeset are not guaranteed to execute in a specific order. Therefore if order matters, you need to define multiple changesets.
You might also wonder “what if I want to get the ID of an item I just created in a batch” for instance if you want to create an order and then a series of line items in an order. You can do this by first getting the ID of the order that was created and then use that in creating the line items. To do this, when creating the order, include another header value that looks like this: Content-ID: 1
. That creates a variable $1
you can use in another request like this: POST [https://../_api/web/lists/getbytitle(‘Orders')/items($1)/LineItems]
or something similar.
OData Batch Responses
So if we issue these requests, what does the response look like? The response comes back looking like a batch as well. It requires a bit of manual processing you can certainly do it. Basically each batch request comes back as a batchresponse and each response includes a header that you can look for, followed by the response headers & a payload of data if applicable. Here’s a snippet of the body from one response when I issued a HTTP POST to create an item followed by a GET query:
--batchresponse_4cc2e5b3-bf5b-42d7-9de3-9a381313d178
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 201 Created
CONTENT-TYPE: application/atom+xml;type=entry;charset=utf-8
ETAG: "1"
LOCATION: https://[your-sharepoint-site]/_api/Web/Lists(guid'3f472226-21b1-4e71-9c91-7dade1aa57b8')/Items(19)
[response as XML]
--batchresponse_4cc2e5b3-bf5b-42d7-9de3-9a381313d178
Content-Type: application/http
Content-Transfer-Encoding: binary
HTTP/1.1 200 OK
CONTENT-TYPE: application/json;odata=verbose;charset=utf-8
[data response from query]
--batchresponse_4cc2e5b3-bf5b-42d7-9de3-9a381313d178--
I cut out the actual responses that came back because it is a lot of data, but you can see the full response from links to same request & the associated responses below. What’s challenging is that according to the OData specification, the response should reference not just the actual batch but the changeset as well. As you can see from creating an item, that is not included. As such, that’s going to make it hard to see what did and did not succeed if a changeset includes multiple creates, deletes or updates. As you can see from section 11.7.4 in the OData v4.0 specification, we should see the changeset listed in the batch. I’ve asked Microsoft about this, so we’ll see where it goes.
Other SharePoint REST API Batch Requests & Responses
I’ve saved the inserts, updates and delete batch requests from a sample app I created to test all this batching out. You can get it from my SharePoint & Office365 REST API Resources repository in GitHub, specifically you want the SpRestBatchSample. What do the requests & responses look like? Here are a few links so you can pick them apart:
- SharePoint Online REST Batch Request Creating Items & Querying List
- SharePoint Online REST Batch Request Updating Items & Querying List
- SharePoint Online REST Batch Request Deleting Items & Querying List
For those who are Fiddler lovers (and aren’t we all?), I’ve run my sample app using single requests as well as batch requests. Here’s what that looked like:
A Few Challenges
At this point I have hit a few snags with the batching implementation on the SharePoint REST API.
First, the fact that changesets aren’t included in the responses presents a bit of a challenge as I mentioned above. Changesets are supported, but because SharePoint is not a transactional system, they only support a single request in each changeset. Still, the response doesn’t list the changesets but since the changesets are processed in order they are received, the responses are also in the order they were processed so you can match the responses up with the requests.
Secondly, there is a good bit of code associated with writing a batch request and processing the response. Thankfully it’s a pretty easy pattern to follow so I think it’s something we could create a library to make our lives a bit easier… that’s something you can be sure to see me blog about soon as I’m working on.