Batch operations
Applies To: # OData client v7 supported OData Client V7
OData Client for .NET supports batch processing of requests to an OData service. This ensures that all operations in the batch are sent to the data service in a single HTTP request, enables the server to process the operations atomically, and reduces the number of round trips to the service.
OData Client for .NET doesn't support sending both query and change in one batch request.
There are 2 ways of invoking batch operations in odata client:
- Batch Query
- Batch Modification
To execute multiple queries in a single batch, you must create each query in the batch as a separate instance of the DataServiceRequest<TElement>
class. The batched query requests are sent to the data service when the ExecuteBatch
method is called. It contains the query request objects.
This method accepts an array of DataServiceRequest
as parameters. It returns a DataServiceResponse
object, which is a collection of QueryOperationResponse<T>
objects that represent responses to individual queries in the batch, each of which contains either a collection of objects returned by the query or error information. When any single query operation in the batch fails, error information is returned in the QueryOperationResponse<T>
object for the operation that failed and the remaining operations are still executed.
DefaultContainer dsc = new DefaultContainer(new Uri("https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
DataServiceQuery<Person> peopleQuery = dsc.People;
DataServiceQuery<Airline> airlinesQuery = dsc.Airlines;
DataServiceResponse batchResponse = dsc.ExecuteBatch(peopleQuery, airlinesQuery);
foreach (OperationResponse r in batchResponse)
{
QueryOperationResponse<Person> people = r as QueryOperationResponse<Person>;
if (people != null)
{
foreach (Person person in people)
{
Console.WriteLine($"Person Name: {person.UserName}");
}
}
QueryOperationResponse<Airline> airlines = r as QueryOperationResponse<Airline>;
if (airlines != null)
{
foreach (Airline airline in airlines)
{
Console.WriteLine($"Airline Name: {airline.Name}");
}
}
}
ExecuteBatch
will send a "POST" request to https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/$batch
. Each internal request contains its own http method "GET".
In .NetCore we can use the await/async syntax as follows.
DataServiceResponse batchResponse = await dsc.ExecuteBatchAsync(peopleQuery, airlinesQuery);
The payload of the request is as following:
--batch_d3bcb804-ee77-4921-9a45-761f98d32029
Content-Type: application/http
Content-Transfer-Encoding: binary
GET https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/People 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: application/http
Content-Transfer-Encoding: binary
GET https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Airlines 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--
In order to batch a set of changes to the server, DataServiceContext
provides SaveChangesOptions.BatchWithSingleChangeset
and SaveChangesOptions.BatchWithIndependentOperations
when SaveChanges
is called.
SaveChangesOptions.BatchWithSingleChangeset
will save changes in a single change set in a batch request. If one request in the batch fails, all requests fail.
SaveChangesOptions.BatchWithIndependentOperations
will save each change independently in a batch request. If one request in the batch fails, the other requests are not affected.
You can refer to odata v4.0 protocol 11.7 to get more details about batch request and whether requests should be contained in one change set or not.
DefaultContainer dsc = new DefaultContainer(new Uri("https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
dsc.MergeOption = MergeOption.PreserveChanges;
Person me = dsc.Me.GetValue();
Trip myTrip = dsc.Me.Trips.First();
me.LastName = "Updated LastName";
myTrip.Description = "Updated Trip";
dsc.UpdateObject(me);
dsc.UpdateObject(myTrip);
dsc.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset);
Console.WriteLine($"Updated LastName: {me.LastName}");
Console.WriteLine($"Updated Trip Description: {myTrip.Description}");
In .NetCore we can use the await/async syntax as follows.
await dsc.SaveChangesAsync(SaveChangesOptions.BatchWithSingleChangeset);
The payload for all requests in one change set is like following
This will send request with URL https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/$batch.
The request headers contain following two headers:
Content-Type: multipart/mixed; boundary=batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8
Accept: multipart/mixed
The request Payload is as following:
--batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8
Content-Type: multipart/mixed; boundary=changeset_b98a784d-af07-4723-9d5c-4722801f4c4d
--changeset_b98a784d-af07-4723-9d5c-4722801f4c4d
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 3
PATCH https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
If-Match: W/"08D24EFA2E435C91"
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services
{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Person","AddressInfo@odata.type":"#Collection(Microsoft.OData.SampleService.Models.TripPin.Location)","AddressInfo":[{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Location","Address":"P.O. Box 555","City":{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.City","CountryRegion":"United States","Name":"Lander","Region":"WY"}}],"Concurrency":635657333837618321,"Emails@odata.type":"#Collection(String)","Emails":["April@example.com","April@contoso.com"],"FirstName":"April","Gender@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.PersonGender","Gender":"Female","LastName":"Test","UserName":"aprilcline"}
--changeset_b98a784d-af07-4723-9d5c-4722801f4c4d
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 4
PATCH https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me/Trips(1001) HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
User-Agent: Microsoft ADO.NET Data Services
{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Trip","Budget":3000,"Description":"Updated Trip","EndsAt":"2014-01-04T00:00:00Z","Name":"Trip in US","ShareId":"9d9b2fa0-efbf-490e-a5e3-bac8f7d47354","StartsAt":"2014-01-01T00:00:00Z","Tags@odata.type":"#Collection(String)","Tags":["Trip in New York","business","sightseeing"],"TripId":1001}
--changeset_b98a784d-af07-4723-9d5c-4722801f4c4d--
--batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8--
There are instances where an odata service is behind a load balancer which usually rewrites the outer URI but not the internal URIs such that the request going out from the client looks like:
POST https://myservice.com/$batch HTTP/1.1
--batch_xyz
GET https://myservice.com/People HTTP/1.1
Content-Type: application/http
Content-Transfer-Encoding: binary
OData-Version: 4.0
BUT when it hits the load balancer it's rewritten to
POST https://loadbalancer/$batch HTTP/1.1 // changed to loadbalancer uri
--batch_xyz
GET https://myservice.com/People HTTP/1.1 // uri in the body remains unchanged
Content-Type: application/http
Content-Transfer-Encoding: binary
OData-Version: 4.0
While the odata service actually expected
POST https://loadbalancer/$batch HTTP/1.1
--batch_xyz
GET https://loadbalancer/People HTTP/1.1
Content-Type: application/http
Content-Transfer-Encoding: binary
OData-Version: 4.0
But since we can't change the internal batch URI to the load balancer URI, we instead get around the problem by using relative URI's to fix this as follows
POST https://loadbalancer/$batch HTTP/1.1
--batch_xyz
GET /People HTTP/1.1 // uses relative uri
Content-Type: application/http
Content-Transfer-Encoding: binary
OData-Version: 4.0
ExecuteBatch
and SaveChanges
defined in DataServiceContext
accept SaveChangesOptions.UseRelativeUri
option which allows individual requests in batch to use Relative URIs. In this case, they will rely on the Base URI of the top level request.
We can pass the SaveChangesOptions.UseRelativeUri
option along with other batch options to ExecuteBatch
method as follows.
var batchResponse = dsc.ExecuteBatch(SaveChangesOptions.BatchWithIndependentOperations | SaveChangesOptions.UseRelativeUri, peopleQuery, airlinesQuery);
The same options apply to ExecuteBatchAsync
method.
We can pass the SaveChangesOptions.UseRelativeUri
option along with other batch options to SaveChanges
method as follows.
dsc.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset | SaveChangesOptions.UseRelativeUri);
The same options apply to SaveChangesAsync
method.
A truncated request Payload will look like this:
--batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8
Content-Type: multipart/mixed; boundary=changeset_b98a784d-af07-4723-9d5c-4722801f4c4d
.....
.....
.....
GET /People HTTP/1.1 // Relative URI is used
OData-Version: 4.0
OData-MaxVersion: 4.0
Accept: application/json;odata.metadata=minimal
.....
.....
PATCH /Me HTTP/1.1 // Relative URI is used
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
.....
.....
.....
PATCH /Me/Trips(1001) HTTP/1.1 // Relative URI is used
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
......
......
......
OData spec has support for Json batch requests https://docs.oasis-open.org/odata/odata-json-format/v4.01/os/odata-json-format-v4.01-os.html#sec_BatchRequestsandResponses
Key attributes for a Json batch request are as follows:
- Payload is in Json format.
- Request headers
Content-Type
andAccept
designates the batch request and response format to be Json batching.
In OData Client, we achieve Json batching by passing SaveChangesOption.UseJsonBatch
flag when calling SaveChanges
or ExecuteBatch
methods.
In ExecuteBatch
var batchResponse = dsc.ExecuteBatch(SaveChangesOptions.BatchWithIndependentOperations | SaveChangesOptions.UseJsonBatch, peopleQuery, airlinesQuery);
In SaveChanges
dsc.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset | SaveChangesOptions.UseJsonBatch);
The payload will be as follows:
https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/$batch
User-Agent: Fiddler
Authorization: <authz token>
Content-Type: application/json
Accept: application/json
Host: localhost:9000
Content-Length: 1234
{
"requests": [
{
"id": "1",
"method": "PATCH",
"atomicityGroup": "06d8a02a-854a-4a21-8e5c-f737bbd2dea8",
"url": "https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me",
"headers": {
"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
"odata-version": "4.0"
},
"body": {"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Person","AddressInfo@odata.type":"#Collection(Microsoft.OData.SampleService.Models.TripPin.Location)","AddressInfo":[{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Location","Address":"P.O. Box 555","City":{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.City","CountryRegion":"United States","Name":"Lander","Region":"WY"}}],"Concurrency":635657333837618321,"Emails@odata.type":"#Collection(String)","Emails":["April@example.com","April@contoso.com"],"FirstName":"April","Gender@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.PersonGender","Gender":"Female","LastName":"Test","UserName":"aprilcline"}
},
{
"id": "2",
"method": "PATCH",
"atomicityGroup": "06d8a02a-854a-4a21-8e5c-f737bbd2dea8",
"url": "https://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me/Trips(1001)",
"headers": {
"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
"odata-version": "4.0"
},
"body": {"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Trip","Budget":3000,"Description":"Updated Trip","EndsAt":"2014-01-04T00:00:00Z","Name":"Trip in US","ShareId":"9d9b2fa0-efbf-490e-a5e3-bac8f7d47354","StartsAt":"2014-01-01T00:00:00Z","Tags@odata.type":"#Collection(String)","Tags":["Trip in New York","business","sightseeing"],"TripId":1001}
}
]
}