My Attempt to Implement Data Concurrency In Dynamics CRM

One of the hardest things to implement in Dynamics CRM is data concurrency. When multiple people are updating the same data, by default CRM will still allow it. But there is always a scenario where we want to prevent this thing and there is no simple way to implement it in various ways (using out-of-the-box feature/custom code).

To enable optimistic-concurrency, CRM already has some API that we can use. But unfortunately, we can’t enable it from UI. Those API:

Inspect CRM Behaviour

Whenever we retrieve from C# (service.Retrieve/service.RetrieveMultiple) or Web Api (Xrm.WebApi.retrieveRecord/Xrm.WebApi.retrieveMultipleRecords), the row version actually already being set.

This is the proof from the C# code (entity.RowVersion):

C# retrieve entity to get RowVersion

This is the proof from WebApi retrieve (data.versionnumber): 

WebApi.retrieveRecord will show versionnumber

The Design

Now knowing that retrieving data from the database can provide us RowVersion. The only problem now is how to submit the current RowVersion vs the database RowVersion. The easiest way for me is to create an attribute to store it. This is the attribute that I create (new_rowversion):

Create attribute new_rowversion with type string

This is the custom-form display:

Layout of the form

The logic that I thinking for it. Whenever a user opens an existing record, we need to retrieve the RowNumber and assign it to the new_rowversion. Then on saving, we need to compare new_rowversion vs entity.RowVersion that we retrieve from the database. This is the flow-chart:

Flowchart

This is the code for Javascript:

var Blog = Blog || {};
var Library = Library || {};
var FORM_TYPE_UPDATE = 2;
(function () {
    this.setRowVersion = function (context) {
        var setRowVersion = function (formContext, data) {
            if (!data) return;
            var rowVersion = data['versionnumber'].toString();
            var attributes = formContext.ui.controls.getAll();
            var rowVersionAttributes = attributes.filter(attribute => attribute.name.indexOf('_rowversion') > -1);
            if (rowVersionAttributes.length === 0) return;
            var attributeName = rowVersionAttributes[0].name;
            formContext.getAttribute(attributeName).setValue(rowVersion);
            formContext.getAttribute(attributeName).setSubmitMode('always');
        };
        var retrieveFn = function (formContext) {
            var dataReference = formContext.data.entity.getEntityReference();
            Xrm.WebApi.retrieveRecord(dataReference.entityType, dataReference.id).
                then(data => setRowVersion(formContext, data),
                    error => console.log(error));
        };
        return function () {
            var formContext = context.getFormContext();
            if (formContext.ui.getFormType() !== FORM_TYPE_UPDATE) return;
            retrieveFn(formContext);
        }();
    };
}).apply(Library);
(function () {
    this.formOnLoad = function (context) {
        Library.setRowVersion(context);
    };
    this.modifiedOnChange = function (context) {
        Blog.formOnLoad(context);
    };
}).apply(Blog);

This is the c# code for the Library:

using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
namespace SharedLibs
{
    public static class ServiceProviderExtension
    {
        public static void ValidateRowVersion(this IServiceProvider serviceProvider)
        {
            var context = (IPluginExecutionContext)
                serviceProvider.GetService(typeof(IPluginExecutionContext));
            if (context.MessageName != "Update") return;
            var serviceFactory = (IOrganizationServiceFactory)
                serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = serviceFactory.CreateOrganizationService(context.UserId);
            var target = context.InputParameters["Target"] as Entity;
            if (target == null) return;
            var data = service.Retrieve(target.LogicalName, target.Id, new ColumnSet(false));
            var rowVersion = target.Attributes.Any(e => e.Key.Contains("_rowversion")) ?
                target.Attributes.FirstOrDefault(e => e.Key.Contains("_rowversion")).Value.ToString() : null;
            var currentRowVersion =  rowVersion ?? target.RowVersion ?? data.RowVersion;
             
            if (currentRowVersion == data.RowVersion) return;
            throw new InvalidPluginExecutionException($"Concurrency error! Database Version: {data.RowVersion}. Target Version: {currentRowVersion}.");
        }
    }
}

This is the c# code for the Plugin:

using Microsoft.Xrm.Sdk;
using SharedLibs;
using System;
namespace Demo.Plugins
{
    public class DemoPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            serviceProvider.ValidateRowVersion();
        }
    }
}

Of course, you need to register for this plugin. I register in Update on PreValidation.

Demonstration

Here is the demonstration:

We open two tabs (the same record) and then try to changes the first tab (success). Then try to change the data from second tab > save. The code showed an error because the RowVersion already changed.

Summary

  • Cons: you need to create a custom attribute (new_rowversion) in each entity (tables) that you want to apply. You need to add that attribute in each form + call Library.setRowVersion on the form onload. And the last one is to add a plugin step in every entity.
  • Pros: support all entities. The code + implementation is not too complex.

What you think?

3 thoughts on “My Attempt to Implement Data Concurrency In Dynamics CRM

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 )

Google photo

You are commenting using your Google 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.