Breaking change in Simple.OData.Client 4.0: OData batch processing

One month ago I announced a new major release of Simple.OData.Client, and in this post I would like to describe one of its few breaking changes: executing OData batch operations.

Earlier versions of Simple.OData.Client had a simple but somewhat naive (and incomplete) support for OData batches. Here’s an example:

using (var batch = new ODataBatch(serviceUri))
{
    var client = new ODataClient(batch);
    await client
        .For<Product>()
        .Set(new { ProductName = "Test3", UnitPrice = 21m })
        .InsertEntryAsync(false);
    await client
        .For<Product>()
        .Set(new { ProductName = "Test4", UnitPrice = 22m })
        .InsertEntryAsync(false);
    batch.Complete();
}

This example works fine because it consists only of insert operations and don’t return anything back. While data modification requests represent a primary use of batch operations, OData batch protocol supports data retrieval (GET) operations that can be mixed with POST/PUT/PATCH/DELETE requests in a single batch. And GET requests didn’t work with earlier Simple.OData.Client versions because its batch API was modeled after SQL transaction API that executes individual statements as they come with possibility to rollback them back. OData batch is a different beast: it accumulates multiple operations and sends them at once as a single HTTP POST request once all operations are written into a batch message. Such behavior prevents responses from individual batch operations to be assigned to local variables before the batch is executed. So the code above succeeds in following “tell don’t ask” principle but it will fail if there is a need to ask about something within a batch.

Batch API in Simple.OData.Client 4 is completely rewritten to properly reflect OData batch behavior. It uses a concept of promises which is implemented using C# lambda expressions. The batch operations are recorded as a sequence of Func delegates having signature Func<IODataClient, Task>. So the statement above will look like this:

var batch = new ODataBatch(serviceUri);
batch += c =&gt; c
    .For&lt;Product&gt;()
    .Set(new { ProductName = "Test3", UnitPrice = 21m })
    .InsertEntryAsync(false);
batch += c =&gt; c
    .For&lt;Product&gt;()
    .Set(new { ProductName = "Test4", UnitPrice = 22m })
    .InsertEntryAsync(false);
await batch.ExecuteAsync();

Note that you don’t need to await batch operations as long as they don’t return any results: the operations are not executed instantly, they are recorded in a single batch message that is sent to the server in ExecuteAsync call.

But what if we need to return some data as a part of a batch? Here’s how it can be achieved:

IEnumerable&lt;Airline&gt; airlines1 = null;
IEnumerable&lt;Airline&gt; airlines2 = null;

var batch = new ODataBatch(client);
batch += async c =&gt; airlines1 = await c
    .For&lt;Airline&gt;()
    .FindEntriesAsync();
batch += c =&gt; c
    .For&lt;Airline&gt;()
    .Set(new Airline() { AirlineCode = "TT", Name = "Test Airline"})
    .InsertEntryAsync(false);
batch += async c =&gt; airlines2 = await c
    .For&lt;Airline&gt;()
    .FindEntriesAsync();
await batch.ExecuteAsync();

This code demonstrates how to implement scenario described in Advanced OData Tutorial at odata.org where the first batch operation retrieves a list of airlines, the second creates a new airline record and the third one refreshes the airline list (remember that it’s up to the server to return the updated or original data collection – OData batches are not database transactions, they don’t conform to ACID principles). Note also the use of async/await pattern in both FindEntriesAsync calls. I wrote earlier that you don’t need to await for operations being recorded but if you want to get hold of their responses you will either have to assign them to variables of type Task<T> or use async/await.

OData batch protocol also defines a way to reference newly created resources so they can be used to create associations with other resources. This scenario is also supported by Simple.OData.Client:

var product1 = new Product() {ProductName = "Test1", UnitPrice = 21m};
var product2 = new Product() {ProductName = "Test2", UnitPrice = 22m};

var batch = new ODataBatch(serviceUri);
batch += c =&gt; c
    .For&lt;Product&gt;()
    .Set(product1)
    .InsertEntryAsync(false);
batch += c =&gt; c
    .For&lt;Product&gt;()
    .Set(product2)
    .InsertEntryAsync(false);
batch += async c =&gt; await c
    .For&lt;Category&gt;()
    .Set(new { CategoryName = "Test3", Products = new[] { product1, product2 } })
    .InsertEntryAsync(false);
await batch.ExecuteAsync();

Here you see how Product entries “product1” and “product2” created in the batch are then referenced in the same batch from another new entry of a type Category that creates a link to a collection of associated products identified by the navigation property “Products”.

The revised batch API is available in Simple.OData.Client 4.0 RC6.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s