Conflicts ========================== Overview ^^^^^^^^^^^^^ Conflicts occurs when a client update / delete / insert a record that is updated / deleted or inserted on the server as well, *before any sync happened*. As an example, we can imagine a conflict occuring during an update on a column called ``Street`: 1) As a starting point, both server and client has a value of ``Street=1 Bellevue Avenue`` after an initial sync (where no conflicts occured). 2) Server is updating the row with a value of "*1 bis Bellevue Avenue*". 3) Client is updating as well the same row value with "*2 Bellevue Avenue*". 4) Sync is launched, and a conflict is raised **on the server side**. Here is the diagram of the situation: .. image:: assets/Conflict01.png By default, conflicts are resolved automaticaly using the configuration policy property ``ConflictResolutionPolicy`` set in the ``SyncOptions`` object : You can choose: * ``ConflictResolutionPolicy.ServerWins`` : The server is the winner of any conflict. this behavior is the default behavior. * ``ConflictResolutionPolicy.ClientWins`` : The client is the winner of any conflict. .. hint:: Default value is ``ServerWins``. .. code-block:: csharp var options = new SyncOptions { ConflictResolutionPolicy = ConflictResolutionPolicy.ServerWins }; Here is the same diagram with the final step, where resolution is set to ``ServerWins`` (default value, by the way) .. image:: assets/Conflict02.png Resolution ^^^^^^^^^^^^^^^^^^^^^^ .. warning:: A conflict is always resolved on the server side. Depending on your policy resolution, the workflow could be: * A conflict is generated on the client and the server side. * The client is launching a sync processus. * The server tries to apply the row and a conflict is generated. * The server resolves the conflict on the server side. * If the server wins, the resolved server row is sent to the client and is *force-applied* on the client database. * If the client wins, the server will *force-apply* the client row on the server. Nothing happen on the client, since the row is correct. Here is the workflow, when the conflict resolution is set to ``ServerWins`` in an **HTTP** mode: .. image:: assets/Conflict03.png Here is the same workflow, when the conflict resolution is now set to ``ClientWins``: .. image:: assets/Conflict04.png Handling conflicts manually ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | If you decide to manually resolve a conflict, the ``ConflictResolutionPolicy`` option will be ignored. | To be able to resolve a conflict, you just have to *Intercept* the ``ApplyChangedFailed`` method and choose the correct version. .. code-block:: csharp agent.OnApplyChangesFailed(args => { // do stuff and choose correct resolution policy }); The ``ApplyChangeFailedEventArgs`` argument contains all the required properties to be able to resolve your conflict. You will determinate the correct version through the `Action` property of type ``ConflictResolution``: .. code-block:: csharp public enum ConflictResolution { /// /// Indicates that the change on the server is the conflict winner /// ServerWins, /// /// Indicates that the change sent by the client is the conflict winner /// ClientWins, /// /// Indicates that you will manage the conflict by filling the final row and sent it to /// both client and server /// MergeRow, /// /// Indicates that you want to rollback the whole sync process /// Rollback } * ``ConflictResolution.ClientWins`` : The client row will be applied on server, even if there is a conflict, so the client row wins. * ``ConflictResolution.ServerWins`` : The client row won't be applied on the server, so the server row wins. * ``ConflictResolution.MergeRow`` : It's up to you to choose the correct row to send on both server and client. the ``FinalRow`` instance will be used instead of Server or Client row. You are able to compare the row in conflict through the ``Conflict`` property of type ``SyncConflict``: * ``Conflict.LocalRow`` : Contains the conflict row from the client side. This row is readonly. * ``Conflict.RemoteRow`` : Contains the conflict row from the server side. This row is readonly. * ``Conflict.Type`` : Gets the ``ConflictType`` enumeration. For example ``ConflictType.RemoteUpdateLocalUpdate`` represents a conflict row beetween an updated row on the server and the same row updated on the client as well. | You can use the current connection during this event to be able to perform actions on the server side through the ``DbConnection`` and ``DbTransaction`` properties. | If you decide to rollback the transaction, all the sync process will be rollback. | Eventually, the ``FinalRow`` property is used when you specify an Action to ``ConflictAction.MergeRow``. | You decide what will contains the row applied on both server and client side. Be careful, the ``FinalRow`` property is null until you specify the ``Action`` property to ``ConflictAction.MergeRow`` ! TCP mode ----------------- Manually resolving a conflict based on a column value: .. code-block:: csharp agent.OnApplyChangesFailed(e => { if (e.Conflict.RemoteRow.Table.TableName == "Region") { e.Action = (int)e.Conflict.RemoteRow["Id"] == 1 ? ConflictResolution.ClientWins : ConflictResolution.ServerWins; } } Manually resolving a conflict based on the conflict type : .. code-block:: csharp agent.OnApplyChangesFailed(args => { switch (args.Conflict.Type) { // case ConflictType.RemoteExistsLocalExists: case ConflictType.RemoteExistsLocalIsDeleted: case ConflictType.RemoteIsDeletedLocalExists: case ConflictType.RemoteIsDeletedLocalIsDeleted: case ConflictType.RemoteCleanedupDeleteLocalUpdate: case ConflictType.RemoteExistsLocalNotExists: case ConflictType.RemoteIsDeletedLocalNotExists: default: break; } }); Resolving a conflict by specifying a merged row : .. code-block:: csharp agent.OnApplyChangesFailed(e => { if (e.Conflict.RemoteRow.Table.TableName == "Region") { e.Action = ConflictResolution.MergeRow; e.FinalRow["RegionDescription"] = "Eastern alone !"; } } .. note:: Be careful, the ``e.FinalRow`` is null until you set the ``Action`` property to ``ConflictAction.MergeRow`` ! HTTP Mode ------------------ We saw that conflicts are resolved on the server side, if you are in an **HTTP** mode, involving a server web side, it is there that you need to intercept failed applied changes: .. code-block:: csharp [Route("api/[controller]")] [ApiController] public class SyncController : ControllerBase { private WebServerAgent webServerAgent; // Injected thanks to Dependency Injection public SyncController(WebServerAgent webServerAgent) => this.webServerAgent = webServerAgent; public async Task Post() { webServerAgent.RemoteOrchestrator.OnApplyChangesFailed(e => { if (e.Conflict.RemoteRow.SchemaTable.TableName == "Region") { e.Resolution = ConflictResolution.MergeRow; e.FinalRow["RegionDescription"] = "Eastern alone !"; } else { e.Resolution = ConflictResolution.ServerWins; } }); // handle request await webServerAgent.HandleRequestAsync(this.HttpContext); } /// /// This Get handler is optional. /// It allows you to see the configuration hosted on the server /// The configuration is shown only if Environmenent == Development /// [HttpGet] public Task Get() => this.HttpContext.WriteHelloAsync(webServerAgent); } Handling conflicts from the client side ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | As we said, all the conflicts are resolved from the server side. | But, using a **Two sync trick**, you are able to resolve the conflict from the client side. Basically the process is occuring in this order: - The first sync will raise the conflict and will be resolved on the server. - The first sync will send back the resolved conflict to the client, containing the server row and the client row - From the client side, you will now be able to ask the client to choose the correct version - The second sync will then send back the *new* version of the row to the server. .. warning:: To be able to use this technic, the ConflictResolutionPolicy MUST be set to ConflictResolutionPolicy.ServerWins Here is a full example using this special trick: .. code-block:: csharp var agent = new SyncAgent(clientProvider, serverProvider, options, setup); var localOrchestrator = agent.LocalOrchestrator; var remoteOrchestrator = agent.RemoteOrchestrator; // Conflict resolution MUST BE set to ServerWins options.ConflictResolutionPolicy = ConflictResolutionPolicy.ServerWins; // From client : Remote is server, Local is client // From here, we are going to let the client decides // who is the winner of the conflict : localOrchestrator.OnApplyChangesFailed(acf => { // Check conflict is correctly set var localRow = acf.Conflict.LocalRow; var remoteRow = acf.Conflict.RemoteRow; // From that point, you can easily letting the client decides // who is the winner // Show a UI with the local / remote row and // letting him decides what is the good row version // for testing purpose; will just going to set name to some fancy BLA BLA value // SHOW UI // OH.... CLIENT DECIDED TO SET NAME TO "BLA BLA BLA" // BE AS FAST AS POSSIBLE IN YOUR DESICION, // SINCE WE HAVE AN OPENED CONNECTION / TRANSACTION RUNNING remoteRow["Name"] = clientNameDecidedOnClientMachine; // Mandatory to override the winner registered in the tracking table // Use with caution ! // To be sure the row will be marked as updated locally, // the scope id should be set to null acf.SenderScopeId = null; }); // First sync, we allow server to resolve the conflict and send back the result to client var s = await agent.SynchronizeAsync(); Assert.Equal(1, s.TotalChangesDownloaded); Assert.Equal(1, s.TotalChangesUploaded); Assert.Equal(1, s.TotalResolvedConflicts); // From this point the Server row Name is STILL "SRV...." // And the Client row NAME is "BLA BLA BLA..." // Make a new sync to send "BLA BLA BLA..." to Server s = await agent.SynchronizeAsync(); Assert.Equal(0, s.TotalChangesDownloaded); Assert.Equal(1, s.TotalChangesUploaded); Assert.Equal(0, s.TotalResolvedConflicts);