30 Days of Zumo.v2 (Azure Mobile Apps): Day 12 – Conflict Resolution

One of the common items that everyone has to deal with is conflict resolution. Let me give you the scenario. I have two devices – a web browser and a Universal Windows App. In both cases, I temporarily hold the list of tasks in memory in a list. I update a specific task on the web browser, maybe changing the title. I update the same task on the app, changing it to a different value. What happens?

Well, the answer is “it depends”.

Let’s take the two cases. In the case of my Universal Windows App, I have the following model:

using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json;

namespace Shellmonger.TaskList.Services
{
    public class TodoItem
    {
        public string Id { get; set; }

        [JsonProperty("text")]
        public string Title { get; set; }

        [JsonProperty("complete")]
        public bool Completed { get; set; }

        [JsonProperty("shared")]
        public bool Shared { get; set; }

        [JsonIgnore]
        public bool NotShared { get { return !Shared;  } }
    }
}

When I update the title, I send a HTTP PATCH with the contents of the model being updated converted to JSON. If I do nothing else, the default conflict resolution is “last write wins”. In the specific case that I mentioned earlier, updating the web browser, then updating the app – with this model – the app change overwrites the browser change. Refreshing the browser will show the new value.

What about the other way round? Because the app does not send a version, the version does not get updated. That means last write wins again.

What if you want to do something else? Like, maybe, pop up an error and allow the user to choose? This is possible too, but all your clients need to support conflict resolution. I need to add an extra field called Version to my model for that:

using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json;

namespace Shellmonger.TaskList.Services
{
    public class TodoItem
    {
        public string Id { get; set; }

        [Version]
        public string Version { get; set; }

        [JsonProperty("text")]
        public string Title { get; set; }

        [JsonProperty("complete")]
        public bool Completed { get; set; }

        [JsonProperty("shared")]
        public bool Shared { get; set; }

        [JsonIgnore]
        public bool NotShared { get { return !Shared;  } }
    }
}

The version string is updated to a new version whenever a POST, PATCH or DELETE operation is performed – i.e., when the record is updated. This is done on the server. When I perform the same sequence of requests now, the server will return a 412 Precondition Failed response. This is trapped by the Azure Mobile Apps Client SDK and a MobileServicePreconditionFailedException is thrown. I can trap this myself like this (from TasksPage.xaml.cs):

            StartNetworkActivity();
            try
            {
                Trace($"taskTitle_Changed - updating title ID={item.Id} Title={item.Title} Complete={item.Completed}");
                await dataTable.UpdateAsync(item);
                TaskListView.Focus(FocusState.Unfocused);
            }
            catch (MobileServicePreconditionFailedException<TodoItem> conflict)
            {
                Trace($"taskTitle_Changed - Conflict Resolution for item ${conflict.Item.Id}");
                var dialog = new MessageDialog(conflict.Item.Id, "Conflict Resolution");
                await dialog.ShowAsync();
            }
            catch (MobileServiceInvalidOperationException exception)
            {
                Trace($"taskTitle_Changed - failed to update title ID={item.Id} Title={item.Title} Error={exception.Message}");
                var dialog = new MessageDialog(exception.Message);
                await dialog.ShowAsync();
            }
            StopNetworkActivity();

Put a breakpoint on the trace and inspect the value of the conflict exception – it contains a TodoItem in the Item property. This contains the server copy of the record. You have your copy of the record already.

At this point you have a couple of choices:

  1. You can choose “Client Wins” – update the version in your record to match the version on the server, then re-push the change
  2. You can choose “Server Wins” – copy the entire record from the server into your list
  3. You can somehow merge the results (see later)
  4. You can let the user choose (I will implement this)

Option #3 is worthy of some discussion. Let’s say you have a complicated model. You could add a hidden “dirty” field to each field you have in your model. As the model gets updated, you identify that the field is dirty. When a conflict happens, you can take the server record as a base, copy the dirty fields into the new record, thus creating a “server + my changes” record, then push that record. It’s a lot more complex to handle, but the process is relatively similar to what I Have below.

That leaves option #4. Leave it up to the user. I can present the user with two options – the server version and the client version. I can let the user decide which one wins. If the user chooses “server”, then I replace the client version with the server version. If the user chooses “client”, then I update the version and push the change to the server. That results in something akin to the following code:

            StartNetworkActivity();
            try
            {
                Trace($"taskTitle_Changed - updating title ID={item.Id} Title={item.Title} Complete={item.Completed}");
                await dataTable.UpdateAsync(item);
                TaskListView.Focus(FocusState.Unfocused);
            }
            catch (MobileServicePreconditionFailedException<TodoItem> conflict)
            {
                Trace($"taskTitle_Changed - Conflict Resolution for item ${conflict.Item.Id}");

                // If the two versions are the same, then ignore the conflict - client wins
                if (conflict.Item.Title.Equals(item.Title) && conflict.Item.Completed == item.Completed)
                {
                    item.Version = conflict.Item.Version;
                }
                else
                {
                    // Build the contents of the dialog box
                    var stackPanel = new StackPanel();
                    var localVersion = new TextBlock
                    {
                        Text = $"Local Version: Title={item.Title} Completed={item.Completed}"
                    };
                    var serverVersion = new TextBlock
                    {
                        Text = $"Server Version: Title={conflict.Item.Title} Completed={conflict.Item.Completed}"
                    };
                    stackPanel.Children.Add(localVersion);
                    stackPanel.Children.Add(serverVersion);

                    // Create the dialog box
                    var dialog = new ContentDialog
                    {
                        Title = "Resolve Conflict",
                        PrimaryButtonText = "Local",
                        SecondaryButtonText = "Server"
                    };
                    dialog.Content = stackPanel;

                    // Show the dialog box and handle the response
                    var result = await dialog.ShowAsync();
                    if (result == ContentDialogResult.Primary)
                    {
                        // Local Version - Copy the version from server to client and re-submit
                        item.Version = conflict.Item.Version;
                        await dataTable.UpdateAsync(item);
                    }
                    else if (result == ContentDialogResult.Secondary)
                    {
                        // Just pull the records from the server
                        await RefreshTasks();
                    }
                }
            }
            catch (MobileServiceInvalidOperationException exception)
            {
                Trace($"taskTitle_Changed - failed to update title ID={item.Id} Title={item.Title} Error={exception.Message}");
                var dialog = new MessageDialog(exception.Message);
                await dialog.ShowAsync();
            }
            StopNetworkActivity();

This is obviously not perfect code. Firstly, I don’t have a great story around what happens if other things (other than the title) change. Secondly, I don’t have a great story for what happens if the second update fails. Finally, I don’t have a good story around the “server” acceptance. I just refresh the tasks. However, this is good enough for you to get the general idea.

Updating the JavaScript app

This does not help me in the JavaScript world. I have an Apache Cordova app and a HTML app that are both based around the HTML/JS Client SDK. In the app, I have the following code:

    /**
     * Event handler for when the user updates the text of a todo item
     * @param {Event} event the event that caused the request
     * @returns {void}
     */
    function updateItemTextHandler(event) {
        var itemId = getTodoItemId(event.currentTarget),
            newText = $(event.currentTarget).val();

        updateSummaryMessage('Updating Item in Azure');
        console.info('updateItemTextHandler: itemId = ', itemId);
        todoItemTable
            .update({ id: itemId, text: newText })  // Async send the update to backend
            .then(refreshDisplay, handleError); // Update the UI
        event.preventDefault();
    }

You can find the code in public/application.js for the web browser version. When you run this, you get the following request sent:

day-12-p1

Note that the request does not have the version included. This means that “last write wins”. The server will do the change, ignoring the version field completely. There are several methods by which this can be fixed. My version is to include the version in the record, like this:

    function createTodoItem(item) {
        return $('<li>')
            .attr('data-todoitem-id', item.id)
            .attr('data-todoitem-version', item.version)
            .append($('<button class="item-delete">Delete</button>'))
            .append($('<input type="checkbox" class="item-complete">')
                .prop('checked', item.complete))
            .append($('<div>')
                .append($('<input class="item-text">').val(item.text)));
    }

I can now write a copy of the getTodoItemId(el) method to grab the version of a record:

    /**
     * Given a sub-element of an LI, find the TodoItem Versuin associated with the list member
     *
     * @param {DOMElement} el the form element
     * @returns {string} the Version of the TodoItem
     */
    function getTodoItemVersion(el) {
        return $(el).closest('li').attr('data-todoitem-version');
    }

Now I can use these in the code to add the version attribute to my update:

    function updateItemTextHandler(event) {
        var itemId = getTodoItemId(event.currentTarget),
            itemVersion = getTodoItemVersion(event.currentTarget),
            newText = $(event.currentTarget).val();

        updateSummaryMessage('Updating Item in Azure');
        console.info('updateItemTextHandler: itemId = ', itemId);
        todoItemTable
            .update({ id: itemId, version: itemVersion, text: newText })  // Async send the update to backend
            .then(refreshDisplay, handleError); // Update the UI
        event.preventDefault();
    }

If there is a conflict, the handleError routine will get called and will show a 412 error.

Next Steps

This is not the last time I will deal with conflict resolution, as this topic is different when covering offline sync. In the next post, I’m going to cover the OData interface and all the errors that you can get from the server SDK. I’ll also take a look at a more reasonable version of the table managers I have been using up to now, providing an overall conflict resolution and two-way binding. Until then, here is the code: