Batch operation ($batch) using OData API Dynamics 365

What is Issue?

There are OData API provided by Microsoft to perform operations on D365 Tables like Create, update, delete and retrieve. These operations can be perform individually by building the OData API using some tools or with your knowledge. But some time there are the cases where you need to call the bulk create, bulk update, bulk delete or some bulk create and bulk update/delete simultaneously. If there are more than one records, you need to hit OData API one by one multiple times which takes cost in terms of time and performance of the application which is calling OData API. Is there a way to combine these all requests in one call and execute?

The Reason-

There is way to perform the CRUD operations on D365 Tables using OData, but everyone aware of the single operations. Microsoft provides the way to perform Batch operation on Tables using OData API. Lets see in solution for this.

Solution-

Microsoft D365 provides the OData for batch operation to perform using “$batch” like-

https://your_org.crm8.dynamics.com/api/data/v8.2/$batch

You need to include the all other CRUD (POST, PATCH, DELETE, GET) OData APIs in the body for the above request URL.

This request works like all other OData APIs using OAuth2.0 authentication with proper headers added.

Lets take .Net application where I need to perform the Create, update and some delete operation in one call.

You have to prepare your request as below-

Preparing the Batch Call-

#region prepare Batch Call
        private string BatchQueryCall(string crmRestQuery, string batchName, string requestBody)
        {
            string funResponse = null;
            try
            {
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, crmRestQuery);
                //add header parameters
                request.Headers.Add("Authorization", "Bearer " + oauthToken);
                request.Headers.Add("Accept", "application/json");
                request.Content = new StringContent(requestBody);
                request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed;boundary=" + batchName);

                HttpResponseMessage response = httpClient.SendAsync(request).Result;
                string responseString = response.Content.ReadAsStringAsync().Result;

                if (response.IsSuccessStatusCode)
                {
                    funResponse = responseString;
                }
                else
                    funResponse = responseString;

            }
            catch (Exception ex)
            {
                funResponse = ex.Message;
            }
            return funResponse;
        }
        #endregion

Preparing to Call Batch Operation-

Suppose I have to create one record, update one record and delete 2 records in same call.
Below code helps you to understand preparing the Batch call-

  #region bulk Operations
        public string BulkOperationsAPI(string entityname)
        {
            #region Local Variable
            string crmRestQuery = apiUrl + "$batch";
            string batchName = "batch_bt123";
            string changeSetVar = "changeset_ch123";
            int coRelId = 1;
            string changeSet1 = null;
            string requestBody = "--" + batchName + Environment.NewLine
                             + "Content-Type:multipart/mixed;boundary=" + changeSetVar + Environment.NewLine + Environment.NewLine + Environment.NewLine;
            #endregion

            #region Create
                //Creating one record
                CreateUpdateOpportunity entity = new CreateUpdateOpportunity();

                entity.transactioncurrencyid_odata_bind = "/transactioncurrencies(c63ba581-6bc6-e811-a96f-000d3af04fb0)";
                entity.estimatedvalue = 1000;
                entity.estimatedclosedate = "2021-01-01";
                entity.name = "Generated From C# Code";
                entity.parentaccountid_odata_bind = "/accounts(3b3b7c71-61d2-ea11-a813-000d3af0205e)";

                changeSet1 = PrepareReuqestBody("POST", changeSetVar, coRelId++, entityname, null, entity);

                if (!string.IsNullOrEmpty(changeSet1))
                    requestBody += changeSet1 + Environment.NewLine;
            
            #endregion

            #region Update
                //updating One record
                CreateUpdateOpportunity entity1 = new CreateUpdateOpportunity();

                entity1.transactioncurrencyid_odata_bind = "/transactioncurrencies(c63ba581-6bc6-e811-a96f-000d3af04fb0)";
                entity1.estimatedvalue = 1000;
                entity1.estimatedclosedate = "2021-01-01";
                entity1.name = "Generated From CSharp Code";
                entity1.parentaccountid_odata_bind = "/accounts(3b3b7c71-61d2-ea11-a813-000d3af0205e)";

                changeSet1 = PrepareReuqestBody("PATCH", changeSetVar, coRelId++, entityname, "20e6bb72-2861-eb11-a812-0022486e93ce", entity1);

                if (!string.IsNullOrEmpty(changeSet1))
                    requestBody += changeSet1 + Environment.NewLine;
            #endregion

            #region Delete
            //delete 1st record  
            changeSet1 = PrepareReuqestBody("DELETE", changeSetVar, coRelId++, entityname, "29e6bb72-2861-eb11-a812-0022486e93ce", null);
                if (!string.IsNullOrEmpty(changeSet1))
                    requestBody += changeSet1 + Environment.NewLine;

            //delete 2nd record
            changeSet1 = PrepareReuqestBody("DELETE", changeSetVar, coRelId++, entityname, "27e6bb72-2861-eb11-a812-0022486e93ce", null);
            if (!string.IsNullOrEmpty(changeSet1))
                requestBody += changeSet1 + Environment.NewLine;
            #endregion


            requestBody += "--" + changeSetVar + "--" + Environment.NewLine;
            requestBody += "--" + batchName + "--";
            
            //format the body URLs
            requestBody = requestBody.Replace("\\/", "/");  

            //call the batch Operations
            return BatchQueryCall(crmRestQuery, batchName, requestBody);
        }
        #endregion

Preparing the body content of Batch call-

In above code there is call made to the function PrepareRequestBody-
This function will prepare the body content of Batch call.

        #region Prepare Request Body
        private string PrepareReuqestBody(string method, string changeset, int contentId, string entityName, string recordid, object entity)
        {

            string x = null;
            switch (method.ToUpper())
            {
                case "PATCH":
                    using (MemoryStream streamOpportunitySerialize = new MemoryStream())
                    {
                        DataContractJsonSerializer ser = new DataContractJsonSerializer(entity.GetType());
                        ser.WriteObject(streamOpportunitySerialize, entity);
                        streamOpportunitySerialize.Position = 0;
                        StreamReader srOpportunity = new StreamReader(streamOpportunitySerialize);
                        string objectJSON = srOpportunity.ReadToEnd();
                        objectJSON = objectJSON.Replace("_odata_bind", "@odata.bind");

                        x = "--" + changeset + Environment.NewLine
                            + "Content-Type: application/http" + Environment.NewLine
                            + "Content-Transfer-Encoding: binary" + Environment.NewLine
                            + "Content-ID: " + contentId + Environment.NewLine + Environment.NewLine
                            + "PATCH " + apiUrl + entityName + "(" + recordid + ") HTTP/1.1" + Environment.NewLine
                            + "Content-Type: application/json;type=entry" + Environment.NewLine
                            + "Accept:application/json" + Environment.NewLine + Environment.NewLine
                            + objectJSON + Environment.NewLine + Environment.NewLine;
                    }
                    break;
                case "POST":
                    using (MemoryStream streamOpportunitySerialize = new MemoryStream())
                    {
                        DataContractJsonSerializer ser = new DataContractJsonSerializer(entity.GetType());
                        ser.WriteObject(streamOpportunitySerialize, entity);
                        streamOpportunitySerialize.Position = 0;
                        StreamReader srOpportunity = new StreamReader(streamOpportunitySerialize);
                        string objectJSON = srOpportunity.ReadToEnd();
                        objectJSON = objectJSON.Replace("_odata_bind", "@odata.bind");

                        x = "--" + changeset + Environment.NewLine
                            + "Content-Type: application/http" + Environment.NewLine
                            + "Content-Transfer-Encoding: binary" + Environment.NewLine
                            + "Content-ID: " + contentId + Environment.NewLine + Environment.NewLine
                            + "POST " + apiUrl + entityName + " HTTP/1.1" + Environment.NewLine
                            + "Content-Type: application/json;type=entry" + Environment.NewLine
                            + "Accept:application/json" + Environment.NewLine + Environment.NewLine
                            + objectJSON + Environment.NewLine + Environment.NewLine;
                    }

                    break;
                case "DELETE":
                    x = "--" + changeset + Environment.NewLine
                    + "Content-Type: application/http" + Environment.NewLine
                    + "Content-Transfer-Encoding: binary" + Environment.NewLine
                    + "Content-ID: " + contentId + Environment.NewLine + Environment.NewLine
                    + "DELETE " + apiUrl + entityName + "(" + recordid + ") HTTP/1.1" + Environment.NewLine
                    + "Accept:application/json" + Environment.NewLine + Environment.NewLine;
                    break;
            }

            return x;
        }
        #endregion

Request Body content will look like-

--batch_bt123
Content-Type:multipart/mixed;boundary=changeset_ch123


--changeset_ch123
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

POST https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities HTTP/1.1
Content-Type: application/json;type=entry
Accept:application/json

{"estimatedclosedate":"2021-01-01","estimatedvalue":1000,"name":"Generated From C# Code","parentaccountid@odata.bind":"/accounts(3b3b7c71-61d2-ea11-a813-000d3af0205e)","transactioncurrencyid@odata.bind":"/transactioncurrencies(c63ba581-6bc6-e811-a96f-000d3af04fb0)"}


--changeset_ch123
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2

PATCH https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(d659b4d6-0f5b-eb11-a812-6045bd727e64) HTTP/1.1
Content-Type: application/json;type=entry
Accept:application/json

{"estimatedclosedate":"2021-01-01","estimatedvalue":1000,"name":"Generated From CSharp Code","parentaccountid@odata.bind":"/accounts(3b3b7c71-61d2-ea11-a813-000d3af0205e)","transactioncurrencyid@odata.bind":"/transactioncurrencies(c63ba581-6bc6-e811-a96f-000d3af04fb0)"}


--changeset_ch123
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 3

DELETE https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(46c9eab1-5ac2-4673-a55a-51cb886db902) HTTP/1.1
Accept:application/json


--changeset_ch123
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 4

DELETE https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(d659b4d6-0f5b-eb11-a812-6045bd727e64) HTTP/1.1
Accept:application/json


--changeset_ch123--
--batch_bt123--

Response recieved will be like-

  --batchresponse_319a4c10-b7fe-4bf0-b4a6-da016d1c0319
Content-Type: multipart/mixed; boundary=changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4

--changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

HTTP/1.1 204 No Content
OData-Version: 4.0
Location: https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(3d4916b4-3861-eb11-a812-0022486e93ce)
OData-EntityId: https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(3d4916b4-3861-eb11-a812-0022486e93ce)


--changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2

HTTP/1.1 204 No Content
OData-Version: 4.0
Location: https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(d659b4d6-0f5b-eb11-a812-6045bd727e64)
OData-EntityId: https://your_org.crm8.dynamics.com/api/data/v8.2/opportunities(d659b4d6-0f5b-eb11-a812-6045bd727e64)


--changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 3

HTTP/1.1 204 No Content
OData-Version: 4.0


--changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 4

HTTP/1.1 204 No Content
OData-Version: 4.0


--changesetresponse_ee30dcdb-1094-4c24-8170-262eae9336a4--
--batchresponse_319a4c10-b7fe-4bf0-b4a6-da016d1c0319--

For Token Generation you can refer the code in my last blog – https://mscrm16tech.com/2021/01/21/crud-operations-on-dynamics-365-crm-tables-with-odata-api/

Hope this will help…
Enjoy Ms CRM!!!

Follow on Facebook- FB: MSCRM16Tech