Whenever you do offline sync, there is a risk of conflicts emerging between client and server updates. Of course the data architecture should be done to minimise this (eg guid based primary keys) but this won’t always eliminate the issue. One example of where this might happen is if the client gets cut off midway through pushing changes to the server. For example if I were to insert a new record on the client, push the changes but before the changes had been confirmed back to the client, the network connection was terminated. The server now has the new record but the client thinks it still needs to send the record – when it does, a conflict arises where both server and client have records with the same primary key. Let’s replicate this and then look at solving it.
I’ll create an AddProperty method into my MainViewModel:
public async Task AddProperty()
{
var table=MobileService.GetSyncTable<RealEstateProperty>();
var prop = new RealEstateProperty
{
Address = “New Random Property”
};
await table.InsertAsync(prop);
await MobileService.SyncContext.PushAsync();
}
Run this, and insert a breakpoint after the InsertAsync but before the PushAsync. At this point inspect the prop object and retrieve the Id. Next, using either Sql Server Management Studio or Visual Studio 2015, connect to the SQL Server instance and run the following query (replacing the Id with the one retrieved in previous step).
insert into realestateinspector.RealEstateProperties (Id,Address,Deleted) Select ‘0a1f8994-4a4b-4548-921a-4da0186b3f6c’,’Not created on client’,0
Now, if I let the PushAsync continue it will fail, causing an exception to be raised.
There are a couple of places that this can be handled. The first is where the call to PushAsync is made – this isn’t great as pushing to the remove service won’t necessarily happen at this point. For example you might insert a record but not push immediately. In this case when you next issue a pull request the push will be done prior to doing the pull. A better way to handle it is to supply a custom MobileServiceSyncHandler as part of the initialization of the sync context:
await MobileService.SyncContext.InitializeAsync(data, new CustomMobileServiceSyncHandler());
The sync handler could look like the following (this is very basic and just drops any conflicts)
public class CustomMobileServiceSyncHandler : MobileServiceSyncHandler
{
public async override Task<JObject> ExecuteTableOperationAsync(IMobileServiceTableOperation operation)
{
try
{
return await base.ExecuteTableOperationAsync(operation);
}
catch (MobileServiceConflictException cex)
{
Debug.WriteLine(cex.Message);
throw;
}
}public override Task OnPushCompleteAsync(MobileServicePushCompletionResult result)
{
foreach (var error in result.Errors)
{
if (error.Status == HttpStatusCode.Conflict)
{
error.CancelAndUpdateItemAsync(error.Result);
error.Handled = true;
}
}
return base.OnPushCompleteAsync(result);
}
}