Performance should always be considered while developing a migration or integration. Generally speaking, the faster you can move the data the better. However, you must take into account the limitations of the platform in order to achieve the highest throughput. In the case of CRM, that means calling the Organization Service as efficiently as possible.
This post isn’t a comprehensive list of all the things you should consider while tuning the performance of your migration/integration. Instead, this post highlights a light-weight approach for running requests that you can use and build upon to enhance performance.
For this post we will be using the ExecuteMultipleRequest from CRM for batching and C# tasks for multi-threading. The ExecuteMultipleRequest does have the limitation of being throttled in CRM Online, so you should not combine ExecuteMultipleRequests with multi-threading if you are targeting CRM Online as you will receive concurrent fault errors.
C# Tasks – https://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx
Execute Multiple Request – https://msdn.microsoft.com/en-us/library/jj863631.aspx
In our sample we are going to create a simple console application that calls the Organization Service using different combinations of threading and batching. In order to run this sample you will need access to an instance of CRM 2013+. Make sure that you target a dev or sandbox environment. Also, it should be noted that this sample does not handle errors or write logs of any kind.
To get started, you will need to download the CRM SDK appropriate for your version of CRM – https://msdn.microsoft.com/en-us/library/hh547453(v=crm.8).aspx
Next, create a console application targeting at least the .NET 4.5 framework. Visual Studio 2012 or higher is recommended.
You will need to add the following references –
- Xrm.Client (from SDK)
- Xrm.Sdk (from SDK)
- Runtime.Serialization (.NET)
In order to take advantage of the multi-threading piece you will need to update your console application’s app.config and increase the allowed concurrent web connections. Add this section inside the <configuration> node –
<system.net> <connectionManagement> <add address=”*” maxconnection=”1000″/> </connectionManagement> </system.net> |
Now replace the standard Program.cs with the following code –
using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Xrm.Client; using Microsoft.Xrm.Client.Services; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query;namespace BulkAccountUpload { class BulkTester { private const string SERVER_URL = “[SERVER_URL]”; private const string USER_NAME = “[USER_NAME]”; private const string PASSWORD = “[PASSWORD]”;public const int BATCH_SIZE = 100;// Controls how many items get passed into an ExecuteMultipleRequest public const int LIST_BATCH_SIZE = 200; // Controls how many items get passed into a thread static void Main(string[] args) using (OrganizationService orgService = new OrganizationService(CrmConnection.Parse(string.Format(“Url={0};Username={1};Password={2}”, SERVER_URL, USER_NAME, PASSWORD)))) Console.WriteLine(); // 2: Multi-threaded with no execute multiple Console.WriteLine(); // 3: Single threaded with execute multiple Console.WriteLine(); // 4: Multi-threaded with execute multiple Console.WriteLine(); Console.WriteLine(“Bulk Upload testing complete. Press Enter key to exit.”); Console.Read(); /// <summary> Console.WriteLine(“Creating Accounts – StartTime: {0} UseExecuteMultiple:{1} UseMultiThread:{2}”, startTime, useExecuteMultiple, useMultiThread); List<OrganizationRequest> requests = new List<OrganizationRequest>(); for (int i = 0; i < 1000; i++) // Account name will be something like BulkTestUploadAccount 0123 CreateRequest request = new CreateRequest(); requests.Add(request); RunRequests(orgService, requests, useExecuteMultiple, useMultiThread); DateTime endTime = DateTime.Now; Console.WriteLine(“Created Accounts – EndTime: {0} TotalTime: {1}”, endTime, endTime – startTime); /// <summary> Console.WriteLine(“Deleting Accounts – StartTime: {0} UseExecuteMultiple:{1} UseMultiThread:{2}”, startTime, useExecuteMultiple, useMultiThread); QueryExpression deleteQuery = new QueryExpression(“account”); DataCollection<Entity> entitiesToDelete = orgService.RetrieveMultiple(deleteQuery).Entities; List<OrganizationRequest> requests = new List<OrganizationRequest>(); foreach (Entity entityToDelete in entitiesToDelete) requests.Add(request); RunRequests(orgService, requests, useMultiThread, useExecuteMultiple); DateTime endTime = DateTime.Now; Console.WriteLine(“Deleted Accounts – EndTime: {0} TotalTime: {1}”, endTime, endTime – startTime); /// <summary> /// <summary> // Create a list of function calls – these functions will be used by Tasks in RunTasks() foreach (List<OrganizationRequest> requestList in splitRequestLists) RunTasks(requestActions); /// <summary> return true; /// <summary> // Get a refresh ExecuteMultiplRequest if (requests.Count > 0) requestCount++; // Have we reached our batch size or did we already hit the total count? // This response will be faulted if there is at least one error // Reset ExecuteMultiple for next batch batchCount++; return true; /// <summary> // Loop through all functions and create a new Task for them tasks[localIndex] = Task.Factory.StartNew(() => actions[localIndex]()); List<AggregateException> exceptions = new List<AggregateException>(); try // TODO: Handle errors in exceptions list /// <summary> executeMultipleRequest.Settings = new ExecuteMultipleSettings() return executeMultipleRequest; /// <summary> // We always round up to the next whole number to ensure all requests are accounted for int currentListIndex = 0; // Add empty lists // Fill empty lists with requests up to the threshold splitRequestList[currentListIndex].Add(request); return splitRequestList; |
You will want to format the document after pasting in the code for readability. Also, you will notice that the Organization Service used here is the Xrm.Client version. Make sure to change the SERVER_URL, USER_NAME, and PASSWORD constants before running the application.
Running the application will create and delete 1000 accounts using a combination of multi-threading and batching. At the end of each operation it will output the total time it took for each operation.
The beginning of the operation is always the same – a list of requests will be created that are then fed into the other methods. Depending on the parameters for batching/multi-threading, the list will be cut up into smaller lists and then distributed amongst multiple threads. These requests will then be batched depending on if the ExecuteMultipleRequest is being used.
You will notice that the result times improve when multi-threading and batching are added to the equation. However, you will eventually hit a max throughput that will be dependent on various factors including – the amount of fields per record, network speed, CRM Online vs On-Premise, machine resources (RAM/CPU), etc.
In general, you will see a noticeable increase in performance with just a few changes to how you call the Organization Service. However, you will still want to make sure you understand the implications of how the addition of batching and multi-threading affects your data. For example, if you are importing accounts, where the Parent Account field is filled out, you will need to do it two passes with creates first followed by updates.
For CRM On-Premise the best option is probably to use a combination of batching and multi-threading. For CRM Online you will either want to use batching on a single thread, making sure that the operation runs at a time when it’s unlikely for another ExecuteMultipleRequest to run at the same time, or do multi-threading without batching.
Hi, a couple of tips
1. Organization Service proxy is *not* thread-safe, however you can cache and re-use IServiceManagement (but you’ll have to create a new service proxy per thread, and using the same IServiceManagement will help speed that up – you can also keep that proxy around on the same thread. It just won’t work on two threads at the same time).
2. for easier multi-threading extension methods check out the PFE Core, we also have a pattern for how to handle multi-threading that could be looked at/borrowed:
– https://pfexrmcore.codeplex.com/
– https://www.nuget.org/packages/Microsoft.Pfe.Xrm.CoreV7