Enforce ConcurrencyBehavior on DataverseServiceClient

Celebrating the GA of DataverseServiceClient, let’s talk about how to implement concurrency (if there are two users updating the same data at the same time, the system must ensure only the first update success and make fail the other) using an out-of-the-box feature. From Dataverse itself, we have UpdateRequest and DeleteRequest messages that have ConcurrencyBehavior property (nothing new here). But, we will learn to force all the developers (if needed) to implement this feature as the default implementation is optional. To make the demonstration simple, we will use the Console app + let’s make it a little bit fancy using the Dependency Injection pattern (part of the solution to implement it)!

Prepare the Project

For your information, I already installed .NET 6.0 SDKs (we will use this version). If you haven’t, you need to install it here.

To create the project, you can open the Command Prompt/ Powershell and run the below command (you can change your directory to the directory you wanted):

dotnet new console -o DataverseClient

Once created, you can CD to the ./DataverseClient folder and type the below command to install all the NuGet packages that we want:

dotnet add package Microsoft.PowerPlatform.Dataverse.Client
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.Abstractions
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Configuration

Next, we will set up all the classes we need!

Create HostBuilder + The DI Setup

Now you can use your favorite code editor. In my case, I’m using Visual Studio > right-click on the project > create a new JSON file named appsettings.json > once created, right-click on the JSON > properties > set the Copy to Output Directory as Copy Always (below is the sample of my appsettings.json):

{
  "Settings": {
    "DataverseConnectionString": "AuthType=OAuth;Username=your-email@domain.com;Password=your-password;Url=https://your-org.crm.dynamics.com/;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97"
  }
}

Next, we need to create a class that will be representing the appsettings.json. Right-click on the the .csproj > create new class name AppSettings.cs (for now it only has 1 property):

namespace DataverseClient;

internal class AppSettings
{
    public string DataverseConnectionString { get; set; } = "";
}

Then, you can right-click the .csproj again > add a new class name Helper.cs:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.PowerPlatform.Dataverse.Client;

namespace DataverseClient;

internal static class Helper
{
    public static IHostBuilder CreateHostBuilder()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureAppConfiguration(builder => 
                builder.AddJsonFile(Directory.GetCurrentDirectory() + "//appsettings.json"))            
            .ConfigureServices((context, services) =>
            {
                var configuration = 
                    context.Configuration.GetSection("Settings").Get<AppSettings>();
                services.AddScoped<IOrganizationServiceAsync>(_ =>
                    new ServiceClient(configuration.DataverseConnectionString));
            });

        return hostBuilder;
    }
}

In the code above, we instruct the Dependency Injector to return the Dataverse ServiceClient when we ask IOrganizationServiceAsync.

Then for the sample testing, in the Program.cs we can use the below code:

using DataverseClient;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;

var builder = Helper.CreateHostBuilder().Build();
var serviceProvider = builder.Services;
var service = serviceProvider.GetRequiredService<IOrganizationServiceAsync>();

var whoAmIResponse = await service.ExecuteAsync(new WhoAmIRequest()) as WhoAmIResponse;

Console.WriteLine(whoAmIResponse?.UserId);

If you run, you can verify that our console is ready to implement heavy stuff 😎:

Simple execution to get WhoAmIResponse

The Solution

By default, if you want to enable the concurrency. The developer needs to do this:

using DataverseClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;

var builder = Helper.CreateHostBuilder().Build();
var serviceProvider = builder.Services;
var service = serviceProvider.GetRequiredService<IOrganizationServiceAsync>();

var contactId = new Guid("d81118b0-50e3-ec11-bb3d-000d3a562245");
var contact = await service.RetrieveAsync("contact",
    contactId, new ColumnSet("firstname", "lastname"));
Console.WriteLine(contact.RowVersion);

var updated = new Entity("contact", contactId);
updated["firstname"] = "Temmy";
updated["lastname"] = "Raharjo";
updated.RowVersion = contact.RowVersion;

var updateRequest = new UpdateRequest
{
    ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches,
    Target = updated
};
await service.ExecuteAsync(updateRequest);

Console.WriteLine("Done update");

The Developer needs to use the UpdateRequest message (if wants to Update) and set the ConcurrencyBehavior to IfRowVersionMatches. If your Developer forgets to add those lines of code, meaning the concurrency behavior will not be set and can lead to a problem in the future.
For alternative and to make it easier, you can create Extension methods for example:

using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;

namespace DataverseClient;

internal static class DataverseServiceExtensions
{
    public static void UpdateWithConcurrency(this IOrganizationService service, Entity updateEntity)
    {
        var updateRequest = new UpdateRequest
        {
            ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches,
            Target = updateEntity
        };

        service.Execute(updateRequest);
    }

    public static async Task UpdateWithConcurrencyAsync(this IOrganizationServiceAsync service, 
        Entity updateEntity)
    {
        var updateRequest = new UpdateRequest
        {
            ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches,
            Target = updateEntity
        };

        await service.ExecuteAsync(updateRequest);
    }
}

Then you can use it like this:

using DataverseClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

var builder = Helper.CreateHostBuilder().Build();
var serviceProvider = builder.Services;
var service = serviceProvider.GetRequiredService<IOrganizationServiceAsync>();

var contactId = new Guid("d81118b0-50e3-ec11-bb3d-000d3a562245");
var contact = await service.RetrieveAsync("contact",
    contactId, new ColumnSet("firstname", "lastname"));
Console.WriteLine(contact.RowVersion);

var updated = new Entity("contact", contactId);
updated["firstname"] = "Temmy";
updated["lastname"] = "Raharjo";
updated.RowVersion = contact.RowVersion;

await service.UpdateWithConcurrencyAsync(updated);

Console.WriteLine("Done update");

But if you want to enforce the Developers use the concurrency. You can create a wrapper class like the below code:

using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace DataverseClient;

internal class ConcurrencyDataverseService : IOrganizationServiceAsync
{
    private readonly IOrganizationServiceAsync _service;

    public ConcurrencyDataverseService(IOrganizationServiceAsync service)
    {
        _service = service;
    }

    public Guid Create(Entity entity)
    {
        return _service.Create(entity);
    }

    public Entity Retrieve(string entityName, Guid id, ColumnSet columnSet)
    {
        return _service.Retrieve(entityName, id, columnSet);
    }

    public void Update(Entity entity)
    {
        Console.WriteLine("Using Concurrency!");
        _service.UpdateWithConcurrency(entity);
    }

    public void Delete(string entityName, Guid id)
    {
        _service.Delete(entityName, id);
    }

    public OrganizationResponse Execute(OrganizationRequest request)
    {
        return _service.Execute(request);
    }

    public void Associate(string entityName, Guid entityId, Relationship relationship, EntityReferenceCollection relatedEntities)
    {
        _service.Associate(entityName, entityId, relationship, relatedEntities);
    }

    public void Disassociate(string entityName, Guid entityId, Relationship relationship,
        EntityReferenceCollection relatedEntities)
    {
        _service.Disassociate(entityName, entityId, relationship, relatedEntities);
    }

    public EntityCollection RetrieveMultiple(QueryBase query)
    {
        return _service.RetrieveMultiple(query);
    }

    public Task<Guid> CreateAsync(Entity entity)
    {
        return _service.CreateAsync(entity);
    }

    public Task<Entity> RetrieveAsync(string entityName, Guid id, ColumnSet columnSet)
    {
        return _service.RetrieveAsync(entityName, id, columnSet);
    }

    public Task UpdateAsync(Entity entity)
    {
        Console.WriteLine("Using Concurrency!");
        return _service.UpdateWithConcurrencyAsync(entity);
    }

    public Task DeleteAsync(string entityName, Guid id)
    {
        return _service.DeleteAsync(entityName, id);
    }

    public Task<OrganizationResponse> ExecuteAsync(OrganizationRequest request)
    {
        return _service.ExecuteAsync(request);
    }

    public Task AssociateAsync(string entityName, Guid entityId, Relationship relationship,
        EntityReferenceCollection relatedEntities)
    {
        return _service.AssociateAsync(entityName, entityId, relationship, relatedEntities);
    }

    public Task DisassociateAsync(string entityName, Guid entityId, Relationship relationship,
        EntityReferenceCollection relatedEntities)
    {
        return _service.DisassociateAsync(entityName, entityId, relationship, relatedEntities);
    }

    public Task<EntityCollection> RetrieveMultipleAsync(QueryBase query)
    {
        return _service.RetrieveMultipleAsync(query);
    }
}

We inject the original implementation from the constructor (lines 11 – 14). And then we create our own implementation in the Update and UpdateAsync method (for the demonstration purpose we only intercept the Update part) with our extensions (lines 26-30 and 68-72).

Then, we need to make changes to our Helper class to connect the ConcurrencyDataverseService with the real class of ServiceClient:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.PowerPlatform.Dataverse.Client;

namespace DataverseClient;

internal static class Helper
{
    public static IHostBuilder CreateHostBuilder()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureAppConfiguration(builder => 
                builder.AddJsonFile(Directory.GetCurrentDirectory() + "//appsettings.json"))            
            .ConfigureServices((context, services) =>
            {
                var configuration = 
                    context.Configuration.GetSection("Settings").Get<AppSettings>();
                services.AddScoped<IOrganizationServiceAsync>(_ =>
                {
                    var client = new ServiceClient(configuration.DataverseConnectionString);
                    return new ConcurrencyDataverseService(client);
                });
            });

        return hostBuilder;
    }
}

With all the changes, now we can try the demo without using our extension anymore

using DataverseClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

var builder = Helper.CreateHostBuilder().Build();
var serviceProvider = builder.Services;
var service = serviceProvider.GetRequiredService<IOrganizationServiceAsync>();

var contactId = new Guid("d81118b0-50e3-ec11-bb3d-000d3a562245");
var contact = await service.RetrieveAsync("contact",
    contactId, new ColumnSet("firstname", "lastname"));
Console.WriteLine(contact.RowVersion);

var updated = new Entity("contact", contactId);
updated["firstname"] = "Temmy";
updated["lastname"] = "Raharjo";
updated.RowVersion = contact.RowVersion;

await service.UpdateAsync(updated);

Console.WriteLine("Done update");

And here is the result:

Make the code simpler + enforce developers to use Concurrency!

Happy CRM-ing!

3 thoughts on “Enforce ConcurrencyBehavior on DataverseServiceClient

  1. Hi Temmy, I’m trying to apply your code.
    I am using vs2019. But the program got a compile error that I can’t solve.

    Some errors shown in vs2019.

    Severity Code Description Project File Line Suppression State
    Error CS8652 The feature ‘global using directive’ is currently in Preview and unsupported. To use Preview features, use the ‘preview’ language version. DataverseClient C:\Users\s.dipietro\source\repos\EnforceConcurrencyBehavior\DataverseClient\obj\Debug\net6.0-windows\DataverseClient.GlobalUsings.g.cs 2 Active

    Severity Code Description Project File Line Suppression State
    Warning NETSDK1182 Targeting .NET 6.0 in Visual Studio 2019 is not supported. DataverseClient C:\Program Files\dotnet\sdk\6.0.106\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.DefaultItems.targets 134

    May you hel me ?

    Thank you very much.

    Salvatore

    Like

      1. Hy Temmy,

        The solution is ok.

        It works now Great.

        i am continuing to implement your example.

        Thank you very much
        Salvatore

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.