Batch requests allow grouping multiple operations into a single HTTP request payload and the service will return a single HTTP response with the response to all operations in the requests. This way, the client can optimize calls to the server and improve the scalability of its service.

Please refer to OData Protocol for more detail about batch, and Batch in ODL for batch in ODL client.

Enable Batch in Web API OData Service

It is very easy to enable batch in an OData service which is built by Web API OData.

Add Batch Handler

public static void Register(HttpConfiguration config)
{
    var odataBatchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
    config.MapODataServiceRoute("odata", "odata", GetModel(), odataBatchHandler);
}

As above, we only need to create a new batch handler and pass it when mapping routing for OData service. Batch will be enabled.

For testing, we can POST a request with batch body to the baseurl/$batch:

POST http://localhost:14409/odata/$batch HTTP/1.1
User-Agent: Fiddler
Host: localhost:14409
Content-Length: 1244
Content-Type: multipart/mixed;boundary=batch_d3bcb804-ee77-4921-9a45-761f98d32029

--batch_d3bcb804-ee77-4921-9a45-761f98d32029
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:14409/odata/Products(0)  HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services

--batch_d3bcb804-ee77-4921-9a45-761f98d32029
Content-Type: multipart/mixed;boundary=changeset_77162fcd-b8da-41ac-a9f8-9357efbbd

--changeset_77162fcd-b8da-41ac-a9f8-9357efbbd 
Content-Type: application/http 
Content-Transfer-Encoding: binary 
Content-ID: 1

DELETE http://localhost:14409/odata/Products(0) HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services

--changeset_77162fcd-b8da-41ac-a9f8-9357efbbd--
--batch_d3bcb804-ee77-4921-9a45-761f98d32029
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:14409/odata/Products  HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services

--batch_d3bcb804-ee77-4921-9a45-761f98d32029--

And the response should be:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: multipart/mixed; boundary=batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
Expires: -1
Server: Microsoft-IIS/8.0
OData-Version: 4.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdXNlcnNcbGlhbndcZG9jdW1lbnRzXHZpc3VhbCBzdHVkaW8gMjAxM1xQcm9qZWN0c1xUZXN0V2ViQVBJUmVsZWFzZVxUZXN0V2ViQVBJUmVsZWFzZVxvZGF0YVwkYmF0Y2g=?=
X-Powered-By: ASP.NET
Date: Wed, 06 May 2015 07:34:29 GMT
Content-Length: 1449

--batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; charset=utf-8
OData-Version: 4.0

{
  "@odata.context":"http://localhost:14409/odata/$metadata#Products/$entity","ID":0,"Name":"0Name"
}
--batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
Content-Type: multipart/mixed; boundary=changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610

--changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

HTTP/1.1 204 No Content


--changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610--
--batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; charset=utf-8
OData-Version: 4.0

{
  "@odata.context":"http://localhost:14409/odata/$metadata#Products","value":[
    {
      "ID":1,"Name":"1Name"
    },{
      "ID":2,"Name":"2Name"
    },{
      "ID":3,"Name":"3Name"
    },{
      "ID":4,"Name":"4Name"
    },{
      "ID":5,"Name":"5Name"
    },{
      "ID":6,"Name":"6Name"
    },{
      "ID":7,"Name":"7Name"
    },{
      "ID":8,"Name":"8Name"
    },{
      "ID":9,"Name":"9Name"
    }
  ]
}
--batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411--

Setting Batch Quotas

DefaultODataBatchHandler contains some configuration, which can be set by customers, to customize the handler. For example, the following code will only allow a maximum of 8 requests per batch and 5 operations per ChangeSet.

var odataBatchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
odataBatchHandler.MessageQuotas.MaxPartsPerBatch = 8;
odataBatchHandler.MessageQuotas.MaxOperationsPerChangeset = 5;

Enable/Disable continue-on-error in Batch Request

We can handle the behavior upon encountering a request within the batch that returns an error by preference odata.continue-on-error, which is specified by OData V4 spec.

Enable Preference odata.continue-on-error

Preference odata.continue-on-error makes no sense by default, and service returns the error for that request and continue processing additional requests within the batch as default behavior.

To enable odata.continue-on-error, please refer to section 4.20 Prefer odata.continue-on-error for details.

Request Without Preference odata.continue-on-error

For testing, we can POST a batch request without Preference odata.continue-on-error:

POST http://localhost:9001/DefaultBatch/$batch HTTP/1.1
Accept: multipart/mixed
Content-Type: multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Host: localhost:9001
Content-Length: 633
Expect: 100-continue
Connection: Keep-Alive

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(0) HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(1) HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0--

The response should be:

HTTP/1.1 200 OK
Content-Length: 820
Content-Type: multipart/mixed; boundary=batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
Server: Microsoft-HTTPAPI/2.0
OData-Version: 4.0
Date: Wed, 12 Aug 2015 02:23:10 GMT

--batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0

{
  "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":0,"Name":"Name 0"
}
--batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8

{"Message":"No HTTP resource was found that matches the request URI 'http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo'.","MessageDetail":"No route data was found for this request."}
--batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda--

Service returned error and stop processing.

Request With Preference odata.continue-on-error

Now POST a batch request with Preference odata.continue-on-error:

POST http://localhost:9001/DefaultBatch/$batch HTTP/1.1
Accept: multipart/mixed
prefer: odata.continue-on-error
Content-Type: multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Host: localhost:9001
Content-Length: 633
Expect: 100-continue
Connection: Keep-Alive

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(0) HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
Content-Type: application/http
Content-Transfer-Encoding: binary

GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(1) HTTP/1.1

--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0--

Service returns the error for that request and continue processing additional requests within the batch:

HTTP/1.1 200 OK
Content-Length: 1190
Content-Type: multipart/mixed; boundary=batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
Server: Microsoft-HTTPAPI/2.0
OData-Version: 4.0
Date: Wed, 12 Aug 2015 02:27:45 GMT

--batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0

{
  "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":0,"Name":"Name 0"
}
--batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8

{"Message":"No HTTP resource was found that matches the request URI 'http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo'.","MessageDetail":"No route data was found for this request."}
--batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
OData-Version: 4.0

{
  "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":1,"Name":"Name 1"
}
--batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11--