Sunday, August 16, 2020

Batch request in Dynamic 365 Crm Web API


Performance in Dynamic 365 Web API using Batch Request. This is not something which I do as regular dynamic crm coding. In one of my projects there was a need to implement such thing to get higher performance as the application was badly hit by the performance.

I have referred to this blog which really help me in developing my code fast and it is well explained.

However, I am writing few additional notes which will helped me and my team to implement such method by keeping some error away while we code.




public static async Task<List<HttpResponseMessage>> SendBatchRequestAsync(List<HttpMessageContent> httpContents, HttpClient _httpClient)
{
    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, _httpClient);
}

public static async Task<List<HttpResponseMessage>> SendBatchRequestAsync(MultipartContent batchContent, HttpClient _httpClient)
{
    HttpRequestMessage batchRequest = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(_httpClient.BaseAddress.AbsoluteUri.Trim() + "$batch")
    };
    batchRequest.Content = batchContent;
    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;
}

public static async Task<List<HttpResponseMessage>> Get_BatchRequestAsync(List<HttpMessageContent> httpContents, HttpClient _httpClient)
{
    // Requests in Change Sets only support the HTTP methods 'POST', 'PUT', 'DELETE', and 'PATCH'.
    // If you provide GET while using ChangeSet : An invalid HTTP method 'GET' was detected for a request in a change set.

    if (httpContents == null)
    {
        throw new ArgumentNullException(nameof(httpContents));
    }
    string batchName = $"batch_{Guid.NewGuid()}";
    MultipartContent batchContent = new MultipartContent("mixed", batchName);
    httpContents.ForEach((c) => batchContent.Add(c));
    return await GetBatchRequestAsync(batchContent, _httpClient);
}

public static async Task<List<HttpResponseMessage>> GetBatchRequestAsync(MultipartContent batchContent, HttpClient _httpClient)
{
    HttpRequestMessage batchRequest = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(_httpClient.BaseAddress.AbsoluteUri.Trim() + "$batch")
    };
    batchRequest.Content = batchContent;
    batchRequest.Headers.Add("OData-MaxVersion", "4.0");
    batchRequest.Headers.Add("OData-Version", "4.0");
    HttpResponseMessage response = await _httpClient.SendAsync(batchRequest);
    MultipartMemoryStreamProvider body = await response.Content.ReadAsMultipartAsync();
    List<HttpResponseMessage> contents = await ReadHttpContents(body);

    return contents; 
}

public static HttpMessageContent CreateHttpMessageContent(HttpClient _httpClient,HttpMethod httpMethod, string requestUri, int contentId = 0, string content = null)
{  
    string baseUrl = _httpClient.BaseAddress.ToString();
    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");
            stringContent.Headers.Add("Prefer", "return=representation");

            requestMessage.Content = stringContent;
        }
        messageContent.Headers.Add("Content-ID", contentId.ToString());
    }
    return messageContent;
}

private static 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 static 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;
}



Example to create Task in Bulk using Batch Request

public async Task<string> CreateTasksInBatch()
{
    List<HttpMessageContent> httpContents = new List();
    for (int i = 1; i <= 5; i++)
    {
        string taskJson = "{\"subject\":\"Task " + i.ToString() + " in batch\",\"regardingobjectid_account_task@odata.bind\":\"/accounts(12900d66-30a0-ea11-a812-000d3a33f58e)\"}";
        httpContents.Add(CreateHttpMessageContent(HttpMethod.Post, "tasks", i, taskJson));
    }
    List<HttpResponseMessage> responses = await SendBatchRequestAsync(httpContents);
}

Example to Fetch Multiple Task and Multiple Account as a part of single Batch Request

public async Task<string> GetTaskAndAccountsInBatch()
{
    List<HttpMessageContent> httpContents = new List<HttpMessageContent>();

    httpContents.Add(CreateHttpMessageContent(HttpMethod.Get, "tasks(3c1e7865-6cca-ea11-a812-000d3a33f58e)?$select=subject"));
    httpContents.Add(CreateHttpMessageContent(HttpMethod.Get, "tasks(3c1e7865-6cca-ea11-a812-000d3a33f58e)?$select=subject"));
    httpContents.Add(CreateHttpMessageContent(HttpMethod.Get, "accounts(12900d66-30a0-ea11-a812-000d3a33f58e)/Account_Tasks?$select=subject"));

    List<HttpResponseMessage> responses = await SendBatchRequestAsync(httpContents);
}