Dataverse: Benchmark ExecuteMultiple Request SDK vs WebApi call
In the PowerApps-Samples repo, I found interesting sample code to execute Dataverse messages using WebAPIs call. For those who ask why there is this option, it is because not so many organizations let you install NuGet Packages freely without having risk analysis (the reason can be anything 😁). And of course, as this is a sample code, you need to modify it to suit your organization's needs. But, this sample code alone intrigues me to compare the performance of the WebAPI call vs the Dataverse SDK. Without further ado! Let's see the implementation.
Benchmark Code
This blog post updated on 24-3-2025 as Jim Daly giving more insight about the benchmark I did. I'm adding another benchmark result as Jim mentioned that the comparision is not apple to apple (CreateMultipleRequest vs ExecuteMultipleRequest).
Below is the benchmark code to create 200 ExecuteMultipleRequestof Contact records:
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Newtonsoft.Json.Linq;
namespace DataverseBenchmarkProject;
[MemoryDiagnoser]
[Config(typeof(Config))]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[SimpleJob(launchCount: 1, warmupCount: 0)]
public class WebApiVsSdkMessages
{
private readonly Service WebApiService;
private readonly ServiceClient SdkServiceClient;
public WebApiVsSdkMessages()
{
#region Optimize Connection
// Change max connections from .NET to a remote service default: 2
System.Net.ServicePointManager.DefaultConnectionLimit = 65000;
// Bump up the min threads reserved for this app to ramp connections faster - minWorkerThreads defaults to 4, minIOCP defaults to 4
ThreadPool.SetMinThreads(100, 100);
// Turn off the Expect 100 to continue message - 'true' will cause the caller to wait until it round-trip confirms a connection to the server
System.Net.ServicePointManager.Expect100Continue = false;
// Can decrease overall transmission overhead but can cause delay in data packet arrival
System.Net.ServicePointManager.UseNagleAlgorithm = false;
#endregion Optimize Connection
SdkServiceClient = Startup
.GetApplicationHost()
.Services.GetService<ServiceClient>()!;
var config = App.InitializeApp();
WebApiService = new Service(config);
}
private readonly int _maxRequestPerBatch = 15;
private readonly int _workerCount = 25;
private readonly int _totalData = 200;
[Benchmark]
public async Task CreateSdkMessage()
{
var requests = new List<CreateRequest>();
for (int i = 0; i < _totalData; i++)
{
requests.Add(new CreateRequest
{
Target = new Entity("contact")
{
["firstname"] = "CreateSdkMessage",
["lastname"] = Guid.NewGuid().ToString(),
},
});
}
var groupData = requests.Chunk(_maxRequestPerBatch).ToArray();
await Parallel.ForEachAsync(
groupData,
new ParallelOptions { MaxDegreeOfParallelism = _workerCount },
async (reqs, cancellationToken) =>
{
var service = SdkServiceClient;
var emr = new ExecuteMultipleRequest
{
Requests = [],
Settings = new ExecuteMultipleSettings
{
ContinueOnError = false,
ReturnResponses = true,
},
};
emr.Requests.AddRange(reqs);
var result = (ExecuteMultipleResponse)
await service.ExecuteAsync(emr, cancellationToken);
Console.WriteLine($"Created CreateSdkMessage {result.Responses.Count}");
}
);
}
[Benchmark]
public async Task CreateWebApi()
{
var requests = new List<JObject>();
for (int i = 0; i < _totalData; i++)
{
var contact = new JObject
{
{ "firstname", "CreateWebApi" },
{ "lastname", Guid.NewGuid().ToString() },
{ "@odata.type", $"Microsoft.Dynamics.CRM.contact" }
};
requests.Add(contact);
}
var groupData = requests.Chunk(_maxRequestPerBatch).ToArray();
await Parallel.ForEachAsync(groupData, new ParallelOptions { MaxDegreeOfParallelism = _workerCount }, async (contacts, token) =>
{
PowerApps.Samples.Messages.CreateMultipleRequest createMultipleRequest = new(entitySetName: "contacts", targets: contacts.ToList());
var result = await WebApiService.SendAsync<PowerApps.Samples.Messages.CreateMultipleResponse>(createMultipleRequest);
Console.WriteLine($"Created CreateWebApi {result.Ids.Length}");
});
}
}
For your information, the sample code only uses a single user to call the WebAPI. Hence, I also changed the initialization of ServiceClient to only use 1 user for SDK calls.
For another test (ExecuteMultipleRequest SDKvs Batch CreateRequest WebAPI), I'm adding the below code:
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace DataverseBenchmarkProject;
[MemoryDiagnoser]
[Config(typeof(Config))]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[SimpleJob(launchCount: 1, warmupCount: 0)]
public class WebApiVsSdkMessages
{
private readonly Service WebApiService;
private readonly ServiceClient SdkServiceClient;
public WebApiVsSdkMessages()
{
#region Optimize Connection
// Change max connections from .NET to a remote service default: 2
System.Net.ServicePointManager.DefaultConnectionLimit = 65000;
// Bump up the min threads reserved for this app to ramp connections faster - minWorkerThreads defaults to 4, minIOCP defaults to 4
ThreadPool.SetMinThreads(100, 100);
// Turn off the Expect 100 to continue message - 'true' will cause the caller to wait until it round-trip confirms a connection to the server
System.Net.ServicePointManager.Expect100Continue = false;
// Can decrease overall transmission overhead but can cause delay in data packet arrival
System.Net.ServicePointManager.UseNagleAlgorithm = false;
#endregion Optimize Connection
SdkServiceClient = Startup
.GetApplicationHost()
.Services.GetService<ServiceClient>()!;
var config = App.InitializeApp();
WebApiService = new Service(config);
}
private readonly int _maxRequestPerBatch = 15;
private readonly int _workerCount = 25;
private readonly int _totalData = 200;
[Benchmark]
public async Task CreateSdkMessage()
{
var requests = new List<CreateRequest>();
for (int i = 0; i < _totalData; i++)
{
requests.Add(new CreateRequest
{
Target = new Entity("contact")
{
["firstname"] = "CreateSdkMessage",
["lastname"] = Guid.NewGuid().ToString(),
},
});
}
var groupData = requests.Chunk(_maxRequestPerBatch).ToArray();
await Parallel.ForEachAsync(
groupData,
new ParallelOptions { MaxDegreeOfParallelism = _workerCount },
async (reqs, cancellationToken) =>
{
var service = SdkServiceClient;
var emr = new ExecuteMultipleRequest
{
Requests = [],
Settings = new ExecuteMultipleSettings
{
ContinueOnError = false,
ReturnResponses = true,
},
};
emr.Requests.AddRange(reqs);
var result = (ExecuteMultipleResponse)
await service.ExecuteAsync(emr, cancellationToken);
Console.WriteLine($"Created CreateSdkMessage {result.Responses.Count}");
}
);
}
[Benchmark]
public async Task BatchCreateRequestWebApi()
{
var requests = new List<HttpRequestMessage>();
for (int i = 0; i < _totalData; i++)
{
var contact = new JObject
{
{ "firstname", "BatchCreateRequestWebApi" },
{ "lastname", Guid.NewGuid().ToString() },
{ "@odata.type", $"Microsoft.Dynamics.CRM.contact" }
};
requests.Add(new PowerApps.Samples.Messages.CreateRequest("contacts", contact));
}
var groupData = requests.Chunk(_maxRequestPerBatch).ToArray();
await Parallel.ForEachAsync(groupData, new ParallelOptions { MaxDegreeOfParallelism = _workerCount }, async (contacts, token) =>
{
PowerApps.Samples.Batch.BatchRequest batchRequest = new(WebApiService.BaseAddress) { Requests = contacts.ToList(), ContinueOnError = true };
var result = await WebApiService.SendAsync<PowerApps.Samples.Batch.BatchResponse>(batchRequest);
Console.WriteLine($"Created BatchCreateRequestWebApi {result.HttpResponseMessages.Count}");
});
}
}
Result
Here is the result of the benchmark on the above code:
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.103
[Host] : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX2 DEBUG
Job-FQGFJT : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX2
LaunchCount=1 WarmupCount=0
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| CreateSdkMessage | 4,561.3 ms | 457.2 ms | 1,289.4 ms | 2.49 MB |
| CreateWebApi | 7,852.8 ms | 252.1 ms | 719.2 ms | 1.46 MB |
As you can see, even the CreateSdkMessage still wins in terms of Mean/Time, the other factors favored in CreateWebApi (CreateMultipleRequest). I can probably still take some time to look at the implementation and see if I can improve something based on the sample code. But, is it worth it to let go of the norm that I have had for years to switch to this WebAPI implementation?
Here is the result on ExecuteMultipleRequest vs BatchCreateRequest:
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.103
[Host] : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX2 DEBUG
Job-IMLOVZ : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX2
LaunchCount=1 WarmupCount=0
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
|---|---|---|---|---|---|---|
| CreateSdkMessage | 3,655.7 ms | 287.0 ms | 828.0 ms | 3,399.7 ms | - | 2.45 MB |
| BatchCreateRequestWebApi | 9,010.1 ms | 306.8 ms | 885.3 ms | 8,862.0 ms | 1000.0000 | 14.31 MB |
What do you think?
Happy CRM-ing! 🚀
Leave a comment
Your comment is sent privately to the author and isn't published on the site.