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.