Thursday, February 14, 2019

Dynamics 365 - How to execute batch operations using the Web API (C# code)

The documentation of Dynamics 365 for Customer Engagement apps version 9.x has a section that specifies the details about how to execute batch operations using the Web API at https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/execute-batch-operations-using-web-api.

Unfortunately it does not provide a sample code (in C#) for how to compose the batch request and handle respective response.

So here is the one that I have built for my project:

public async Task<List<HttpResponseMessage>> SendBatchRequestAsync(List<HttpMessageContent> httpContents)
{
if (httpContents == null)
{
throw new ArgumentNullException(nameof(httpContents));
}
string batchName = $"batch_{Guid.NewGuid()}";
MultipartContent batchContent = new MultipartContent("mixed", batchName);
string changesetName = $"changeset_{Guid.NewGuid()}";
MultipartContent changesetContent = new MultipartContent("mixed", changesetName);
httpContents.ForEach((c) => changesetContent.Add(c));
batchContent.Add(changesetContent);
return await SendBatchRequestAsync(batchContent);
}
public async Task<List<HttpResponseMessage>> SendBatchRequestAsync(MultipartContent batchContent)
{
HttpRequestMessage batchRequest = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(_httpClient.BaseAddress.AbsoluteUri.Trim() + "$batch")
};
batchRequest.Content = batchContent;
batchRequest.Headers.Add("Prefer", Settings.ClientOptions.DefaultRequestHeaders["Prefer"]);
batchRequest.Headers.Add("OData-MaxVersion", "4.0");
batchRequest.Headers.Add("OData-Version", "4.0");
batchRequest.Headers.Add("Accept", "application/json");
HttpResponseMessage response = await _httpClient.SendAsync(batchRequest);
MultipartMemoryStreamProvider body = await response.Content.ReadAsMultipartAsync();
List<HttpResponseMessage> contents = await ReadHttpContents(body);
return contents;
}
private async Task<List<HttpResponseMessage>> ReadHttpContents(MultipartMemoryStreamProvider body)
{
List<HttpResponseMessage> results = new List<HttpResponseMessage>();
if (body?.Contents != null)
{
foreach (HttpContent c in body.Contents)
{
if (c.IsMimeMultipartContent())
{
results.AddRange(await ReadHttpContents((await c.ReadAsMultipartAsync())));
}
else if (c.IsHttpResponseMessageContent())
{
HttpResponseMessage responseMessage = await c.ReadAsHttpResponseMessageAsync();
if (responseMessage != null)
{
results.Add(responseMessage);
}
}
else
{
HttpResponseMessage responseMessage = DeserializeToResponse(await c.ReadAsStreamAsync());
if (responseMessage != null)
{
results.Add(responseMessage);
}
}
}
}
return results;
}
private HttpResponseMessage DeserializeToResponse(Stream stream)
{
HttpResponseMessage response = new HttpResponseMessage();
MemoryStream memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
response.Content = new ByteArrayContent(memoryStream.ToArray());
response.Content.Headers.Add("Content-Type", "application/http;msgtype=response");
return response.Content.ReadAsHttpResponseMessageAsync().Result;
}
The SendBatchRequestAsync method has two overloads which can be used for two different scenarios:
  • Execute a batch request that is composed of a list of individual requests, each of them can be in turn a change set, or
  • Execute a list of requests that are grouped into a single change set.
Example 1:
string batchName = $"batch_{Guid.NewGuid()}";
MultipartContent batchContent = new MultipartContent("mixed", batchName);
string changesetName = $"changeset_{Guid.NewGuid()}";
MultipartContent changesetContent = new MultipartContent("mixed", changesetName);
string taskJson1 = "{\"subject\":\"Task 1 in batch\",\"regardingobjectid_account_task@odata.bind\":\"[Organization URI]/api/data/v9.0/accounts(00000000-0000-0000-000000000001)\"}";
changesetContent.Add(CreateHttpMessageContent(HttpMethod.Post, "tasks", 1, taskJson1));
string taskJson2 = "{\"subject\":\"Task 2 in batch\",\"regardingobjectid_account_task@odata.bind\":\"[Organization URI]/api/data/v9.0/accounts(00000000-0000-0000-000000000001)\"}";
changesetContent.Add(CreateHttpMessageContent(HttpMethod.Post, "tasks", 2, taskJson2));
batchContent.Add(changesetContent);
batchContent.Add(CreateHttpMessageContent(HttpMethod.Get, "Account_Tasks?$select=subject"));
List<HttpResponseMessage> responses = await SendBatchRequestAsync(batchContent);

Example 2:
List<HttpMessageContent> httpContents = new List<HttpMessageContent>();
for (int i = 1; i <= 5; i++)
{
string taskJson = "{\"subject\":\"Task " + i.ToString() + " in batch\",\"regardingobjectid_account_task@odata.bind\":\"[Organization URI]/api/data/v9.0/accounts(00000000-0000-0000-000000000001)\"}";
httpContents.Add(CreateHttpMessageContent(HttpMethod.Post, "tasks", i, taskJson));
}
List<HttpResponseMessage> responses = await SendBatchRequestAsync(httpContents);

Utility method to create HttpMessageContent:
public HttpMessageContent CreateHttpMessageContent(HttpMethod httpMethod, string requestUri, int contentId = 0, string content = null)
{
string baseUrl = _httpClient.BaseAddress.AbsoluteUri.Trim();
if (!requestUri.StartsWith(baseUrl))
{
requestUri = baseUrl + requestUri;
}
HttpRequestMessage requestMessage = new HttpRequestMessage(httpMethod, requestUri);
HttpMessageContent messageContent = new HttpMessageContent(requestMessage);
messageContent.Headers.Remove("Content-Type");
messageContent.Headers.Add("Content-Type", "application/http");
messageContent.Headers.Add("Content-Transfer-Encoding", "binary");
// only GET request requires Accept header
if (httpMethod == HttpMethod.Get)
{
requestMessage.Headers.Add("Accept", "application/json");
}
else
{
// request other than GET may have content, which is normally JSON
if (!string.IsNullOrEmpty(content))
{
StringContent stringContent = new StringContent(content);
stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json;type=entry");
requestMessage.Content = stringContent;
}
messageContent.Headers.Add("Content-ID", contentId.ToString());
}
return messageContent;
}

5 comments:

  1. Really appreciate your efforts. Well done.

    ReplyDelete
  2. I have also created a working code at https://github.com/thucnguyen77/dynamics-365-web-api-batch-request-example.

    ReplyDelete
  3. Thank you very much. you save my time :)

    ReplyDelete
  4. Does this allows multiple files to be uploaded using the batch requests?

    ReplyDelete