Implementing Request Batching

As described in Data Access for Client Applications, request batching is a useful way of minimizing the number of messages that are passed between the client and the server. This reduces network traffic and provides a smoother, less chatty user interface. Both the client-side object model (CSOM) and the SharePoint REST interface support batched requests, although they implement batching in very different ways. The Client Reference Implementation (Client RI) demonstrates request batching with both the CSOM and the REST interface.

Request Batching with the CSOM

The CSOM programming model is built around request batching. When you work with the CSOM, you can perform a series of data operations on the ClientContext object. These operations are submitted to the server in a single request when you call the ClientContext.BeginExecuteQuery method. You can also use the ClientContext.Load method to tell the client context object what to return when it executes a batched request. The following code example, taken from the PartInventoryViewModel class in the Silverlight CSOM interface, shows a set of batched CSOM operations using the Silverlight client API.

List partsList = clientContext.Web.Lists.GetByTitle("Parts");
List inventoryLocationsList = clientContext.Web.Lists
                                .GetByTitle("Inventory Locations");

CamlQuery camlQueryPartsList = new CamlQuery();
camlQueryPartsList.ViewXml = 
  @"<View>
      <Query>
        <Where>
          <BeginsWith>
            <FieldRef Name='SKU' />
            <Value Type='Text'>" + SearchSku + @"</Value>
          </BeginsWith>
        </Where>
      </Query>
    </View>";

CamlQuery camlQueryInvLocationList = new CamlQuery();
camlQueryInvLocationList.ViewXml = 
  @"<View>
      <Query>
        <Where>
          <BeginsWith>
            <FieldRef Name='PartLookupSKU' />
            <Value Type='Lookup'>" + SearchSku + @"</Value>
          </BeginsWith>
        </Where>
        <OrderBy Override='TRUE'>
          <FieldRef Name='PartLookupSKU' />
        </OrderBy>
      </Query>
      <ViewFields>
        <FieldRef Name='PartLookup' LookupId='TRUE' />
        <FieldRef Name='PartLookupSKU' />
        <FieldRef Name='PartLookupTitle' />
        <FieldRef Name='PartLookupDescription' />
        <FieldRef Name='BinNumber' />
        <FieldRef Name='Quantity' />
      </ViewFields>
      <ProjectedFields>
        <Field Name='PartLookupSKU' Type='Lookup' List='PartLookup' 
               ShowField='SKU' />
        <Field Name='PartLookupTitle' Type='Lookup' List='PartLookup' 
               ShowField='Title' />
        <Field Name='PartLookupDescription' Type='Lookup' List='PartLookup' 
               ShowField='PartsDescription' />
      </ProjectedFields>
      <Joins>
        <Join Type='LEFT' ListAlias='PartLookup'>
          <!--List Name: Parts-->
          <Eq>
            <FieldRef Name='PartLookup' RefType='ID' />
            <FieldRef List='PartLookup' Name='ID' />
          </Eq>
        </Join>
      </Joins>
    </View>";

partListItems = partsList.GetItems(camlQueryPartsList);
inventoryLocationListItems = 
  inventoryLocationsList.GetItems(camlQueryInvLocationList);

clientContext.Load(partListItems);
clientContext.Load(inventoryLocationListItems);
clientContext.ExecuteQueryAsync(onQuerySucceeded, onQueryFailed);

Note

partListItems and inventoryLocationListItems are local variables of type ListItemCollection. The clientContext object is instantiated with the site URL in the view model constructor.

In this example, the following actions are batched on the client:

  • The Parts list is retrieved by title.
  • The Inventory Locations list is retrieved by title.
  • A CAML query is executed against the Parts list.
  • A CAML query is executed against the Inventory Locations list. This query uses a left outer join across the Inventory Locations list and the Parts list, and returns a view projection.
  • The results of both CAML queries are loaded into the client context object as list item collections.

However, none of these actions are sent to the server and executed until the call to ExecuteQueryAsync is made at the bottom of the code example. When the server responds to the batched request, the partListItems and inventoryLocationListItems collections are populated with the query results. At this point, we can parse these collections to update the user interface, as shown by the following code example.

private void DisplayParts()
{
  List<int> inventoryPartResults = new List<int>();

  //Populate BindingViewsModels with Parts with InventoryLocations
  foreach (ListItem inventoryLocationListItem in inventoryLocationListItems)
  {
    PartInventory view = new PartInventory();
    view.InventoryItem.Id = 
      int.Parse(inventoryLocationListItem["ID"].ToString());
    view.InventoryItem.Quantity = 
      int.Parse(inventoryLocationListItem["Quantity"].ToString());
    view.InventoryItem.BinNumber = 
      inventoryLocationListItem["BinNumber"].ToString();
    view.Part.SKU = ((FieldLookupValue)
      inventoryLocationListItem["PartLookupSKU"]).LookupValue;
    view.Part.Title = ((FieldLookupValue)
      inventoryLocationListItem["PartLookupTitle"]).LookupValue;
    view.Part.Id = ((FieldLookupValue)
      inventoryLocationListItem["PartLookup"]).LookupId;
    view.Part.Description = ((FieldLookupValue)
      inventoryLocationListItem["PartLookupDescription"]).LookupValue;

    Parts.Add(view);
    inventoryPartResults.Add(view.Part.Id);
  }
  ...

Request Batching with REST

You can also batch requests to the SharePoint REST interface. REST is underpinned by the OData protocol, which allows you to batch requests by creating a multi-part MIME format request. The DataServiceContext class provides a method, BeginExecuteBatch, which you can use to submit multiple queries to the REST service. The following code example, taken from the PartInventoryViewModel class in the Silverlight REST interface, shows a set of batched REST operations using the Silverlight client API.

public void RefreshSearchSku(string Sku)
{
  SearchSku = Sku;

  var inventoryLocationsQuery = 
    (DataServiceQuery<InventoryLocationsItem>)context.InventoryLocations
      .Expand("Part")
      .AddQueryOption("$select", 
          "BinNumber,Quantity,Title,Id,PartId,Part/SKU,Part/Title,Part/Id")
      .Where(p => p.Part.SKU.StartsWith(SearchSku))
      .OrderBy(p => p.Part.SKU);

  var partsQuery = 
    (DataServiceQuery<PartsItem>)context.Parts
      .Where(p => p.SKU.StartsWith(SearchSku))
      .Select(p => new PartsItem 
        { 
          Title = p.Title, 
          SKU = p.SKU, 
          Id = p.Id, 
          Description = p.Description 
        });

  //Execute Query as a Batch
   context.BeginExecuteBatch(DisplayParts, context, inventoryLocationsQuery,  
                             partsQuery);
}

The BeginExecuteBatch method accepts a params array of DataServiceRequest objects, so you can submit as many simultaneous queries as you want. However, unlike the CSOM approach, you must manually parse the response in order to retrieve the query results. The following code shows the DisplayParts callback method for the batched request.

private void DisplayParts(IAsyncResult result)
{
  Dispatcher.BeginInvoke(() =>
  {
    Parts.Clear();

    List<PartsItem> AllPartResults = new List<PartsItem>();
    List<PartsItem> NoInventoryPartResults = new List<PartsItem>();

    //Get the Batch Response
    DataServiceResponse Response = context.EndExecuteBatch(result);

    //Loop through each operation
    foreach (QueryOperationResponse Operation in Response)
    {
      if (Operation.Error != null)
      {
        throw Operation.Error;
      }
      if (Operation is QueryOperationResponse<InventoryLocationsItem>)
      {
        //Process Results
        foreach (InventoryLocationsItem location in Operation as 
                   QueryOperationResponse<InventoryLocationsItem>)
        {
          PartInventory partInventory = new PartInventory();
          partInventory.Part = location.Part;
          partInventory.InventoryItem = location;
          Parts.Add(partInventory);
          InventoryPartResults.Add(location.Part);
        }
      }
      if (Operation is QueryOperationResponse<PartsItem>)
      {
        //Process Results
        foreach (PartsItem part in Operation as 
                   QueryOperationResponse<PartsItem>)
        {
          AllPartResults.Add(part);
        }
      }
    }

    foreach (var allPartResult in AllPartResults
               .Where(allPartResult => !InventoryPartResults.Contains(allPartResult)))
    {
      NoInventoryPartResults.Add(allPartResult);
    }

    foreach (var part in NoInventoryPartResults)
    {
      PartInventory partInventory = new PartInventory();
      partInventory.Part = part;
      partInventory.InventoryItem = null;
      Parts.Add(partInventory);
    }
  });
}

In this case, the callback method iterates through a set of query responses. It uses the type of each query response to make a decision on how to process the result set.

Update Batching

When you use the CSOM or the REST interface, the context object manages change tracking for you. The Client.CSOM.Silverlight and Client.REST.Silverlight projects take advantage of this behavior to support the batching of updates. The following code shows a list update using the CSOM.

public void UpdateInventoryLocation()
{
  List inventoryLocationsList = 
    clientContext.Web.Lists.GetByTitle("Inventory Locations");
  
  ListItem inventoryLocation = null;
  inventoryLocation =   
    inventoryLocationsList.GetItemById(currentItem.InventoryItem.Id);
  inventoryLocation["BinNumber"] = currentItem.InventoryItem.BinNumber;
  inventoryLocation["Quantity"] = currentItem.InventoryItem.Quantity;
  inventoryLocation.Update();
}

Notice that the BeginExecute method is not called on the client context object. Therefore, these updates remain in memory on the client and are not immediately sent to the server. The user can update many items through the user interface without causing a request to be sent to the server. When the user clicks the Save Changes button in the user interface, all of the pending changes are committed to SharePoint simultaneously.

public void Update()
{
  clientContext.ExecuteQueryAsync(onUpdatePartLocationSuccess, onQueryFailed);
}

The REST implementation works in a similar way, although in this case the REST service proxy, PartsDataContext, tracks the changes.

public void UpdateInventoryLocation()
{
  context.UpdateObject(CurrentItem.InventoryItem);
}

When the user clicks Save Changes, the code commits all the pending changes to SharePoint by calling the BeginSaveChanges on the context object. Notice that the SaveChangesOptions.Batch argument is added to indicate that this is a batch update.

public void Update()
{
  context.BeginSaveChanges(SaveChangesOptions.Batch, OnSaveChanges, null);
}

In general, batched operations provide a better user experience and make more efficient use of network bandwidth and server resources.