Dataverse ServiceClient: Comparing ExecuteAsync vs Execute
This is another benchmark blog post, in which I tried to upgrade the greatest snippet on how to do bulk insert, update, or delete (blog post by Mark Carrington that you can read here) with the async operation. As you know, the latest Dataverse ServiceClientsupports async operations, and theoretically, if we use those async operations, it will boost the performance as async will utilize the resources better.
Benchmark Code
The code:
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
namespace DataverseBenchmarkProject;
[MemoryDiagnoser]
[Config(typeof(Config))]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)]
[SimpleJob(launchCount: 1, warmupCount: 0)]
public class CreateEntitiesBenchmark
{
public CreateEntitiesBenchmark()
{
var connectionStrings = Startup.GetApplicationHost().Services.GetService<ConnectionString>()!;
_xrmConnection = new XrmConnection(connectionStrings);
}
private readonly XrmConnection _xrmConnection;
private readonly int _maxRequestPerBatch = 15;
private readonly int _workerCount = 25;
private readonly int _totalData = 400;
public CreateRequest GenerateRequest(string info)
{
return new CreateRequest
{
Target = new Entity("contact")
{
["firstname"] = info,
["lastname"] = Guid.NewGuid().ToString()
}
};
}
[Benchmark]
public void MarkCarringtonExecuteMultipleRequest()
{
var requests = new List<CreateRequest>();
for (int i = 0; i < _totalData; i++)
{
requests.Add(GenerateRequest("MarkCarrington"));
}
Parallel.ForEach(requests,
new ParallelOptions { MaxDegreeOfParallelism = _workerCount },
() => new
{
Service = _xrmConnection.GetServiceClient(),
EMR = new ExecuteMultipleRequest
{
Requests = [],
Settings = new ExecuteMultipleSettings
{
ContinueOnError = false,
ReturnResponses = true
}
}
},
(req, loopState, index, threadLocalState) =>
{
threadLocalState.EMR.Requests.Add(req);
if (threadLocalState.EMR.Requests.Count == _maxRequestPerBatch)
{
var result = (ExecuteMultipleResponse)threadLocalState.Service.Execute(threadLocalState.EMR);
Console.WriteLine($"Created MarkCarringtonExecuteMultipleRequest {result.Responses.Count}");
threadLocalState.EMR.Requests.Clear();
}
return threadLocalState;
},
(threadLocalState) =>
{
if (threadLocalState.EMR.Requests.Count > 0)
{
var result = (ExecuteMultipleResponse)threadLocalState.Service.Execute(threadLocalState.EMR);
Console.WriteLine($"Created MarkCarringtonExecuteMultipleRequest {result.Responses.Count}");
}
});
}
[Benchmark]
public void ParallelForEachAsync()
{
var requests = new List<CreateRequest>();
for (int i = 0; i < _totalData; i++)
{
requests.Add(GenerateRequest("ParallelForEachAsync"));
}
var groupData = requests.Chunk(_maxRequestPerBatch).ToArray();
var parent = Parallel.ForEachAsync(groupData,
new ParallelOptions { MaxDegreeOfParallelism = _workerCount },
async (reqs, cancellationToken) =>
{
var service = _xrmConnection.GetServiceClient();
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 ParallelForEachAsync {result.Responses.Count}");
});
parent.Wait();
}
}
As you can see in the above, the implementation a little bit changed as on ParallelForEachAsync we are using the Chunkmethod and split the request by _maxRequestPerBatch. Then, in line 94 we declared an async method that receives the reqs and cancellationToken parameters. On line 107, we use ExecuteAsync to process the requests. And at last, on line 111, we make sure to wait for the parent Task to complete the execution.
Based on the above benchmark, here is the result:
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2894)
AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.100
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
Job-KCBJEO : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
LaunchCount=1 WarmupCount=0
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|
| ParallelForEachAsync | 4,044.5 ms | 138.2 ms | 378.4 ms | 2000.0000 | 1000.0000 | 16.93 MB |
| MarkCarringtonExecuteMultipleRequest | 27,847.3 ms | 2,103.6 ms | 6,136.3 ms | 2000.0000 | 1000.0000 | 16.95 MB |
Benchmark Result
Hopefully, this gives you a reason to start migrating CrmServiceClientto Dataverse ServiceClient. Happy CRM-ing! 🚀
Leave a comment
Your comment is sent privately to the author and isn't published on the site.