/// <summary> /// Apply changes : Insert / Updates Delete /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, ChangesApplied)> ApplyChangesAsync(SyncContext context, MessageApplyChanges message) { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; DbConnection connection = null; ChangesApplied changesApplied = new ChangesApplied(); try { using (connection = this.CreateConnection()) { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, message.Schema, connection, applyTransaction, message.FromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during reset tables", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.FromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying deletes", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 2) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying updates", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } // ----------------------------------------------------- // 3) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying inserts", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } applyTransaction.Commit(); return(context, changesApplied); } } catch (SyncException) { throw; } catch (Exception ex) { throw new SyncException(ex, SyncStage.TableChangesApplying, this.ProviderTypeName); } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (message.Changes != null) { message.Changes.Clear(); } } }
/// <summary> /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter /// </summary> internal ChangeApplicationAction ApplyChangesInternal( SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, DmRowState applyType, ChangesApplied changesApplied) { ChangeApplicationAction changeApplicationAction = ChangeApplicationAction.Continue; // for each adapters (Zero to End for Insert / Updates -- End to Zero for Deletes for (int i = 0; i < message.Schema.Tables.Count; i++) { // If we have a delete we must go from Up to Down, orthewise Dow to Up index var tableDescription = (applyType != DmRowState.Deleted ? message.Schema.Tables[i] : message.Schema.Tables[message.Schema.Tables.Count - i - 1]); // if we are in upload stage, so check if table is not download only if (context.SyncWay == SyncWay.Upload && tableDescription.SyncDirection == SyncDirection.DownloadOnly) { continue; } // if we are in download stage, so check if table is not download only if (context.SyncWay == SyncWay.Download && tableDescription.SyncDirection == SyncDirection.UploadOnly) { continue; } var builder = this.GetDatabaseBuilder(tableDescription); var syncAdapter = builder.CreateSyncAdapter(connection, transaction); syncAdapter.ConflictApplyAction = SyncConfiguration.GetApplyAction(message.Policy); // Set syncAdapter properties syncAdapter.applyType = applyType; // Get conflict handler resolver if (syncAdapter.ConflictActionInvoker == null && this.ApplyChangedFailed != null) { syncAdapter.ConflictActionInvoker = GetConflictAction; } if (message.Changes.BatchPartsInfo != null && message.Changes.BatchPartsInfo.Count > 0) { // getting the table to be applied // we may have multiple batch files, so we can have multipe dmTable with the same Name // We can say that dmTable may be contained in several files foreach (DmTable dmTablePart in message.Changes.GetTable(tableDescription.TableName)) { if (dmTablePart == null || dmTablePart.Rows.Count == 0) { continue; } // check and filter var dmChangesView = new DmView(dmTablePart, (r) => r.RowState == applyType); if (dmChangesView.Count == 0) { dmChangesView.Dispose(); dmChangesView = null; continue; } // Conflicts occured when trying to apply rows List <SyncConflict> conflicts = new List <SyncConflict>(); // Raise event progress only if there are rows to be applied context.SyncStage = SyncStage.TableChangesApplying; var args = new TableChangesApplyingEventArgs(this.ProviderTypeName, context.SyncStage, tableDescription.TableName, applyType); this.TryRaiseProgressEvent(args, this.TableChangesApplying); int rowsApplied; // applying the bulkchanges command if (message.UseBulkOperations && this.SupportBulkOperations) { rowsApplied = syncAdapter.ApplyBulkChanges(dmChangesView, message.FromScope, conflicts); } else { rowsApplied = syncAdapter.ApplyChanges(dmChangesView, message.FromScope, conflicts); } // If conflicts occured // Eventuall, conflicts are resolved on server side. if (conflicts != null && conflicts.Count > 0) { foreach (var conflict in conflicts) { //var scopeBuilder = this.GetScopeBuilder(); //var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(message.ScopeInfoTableName, connection, transaction); //var localTimeStamp = scopeInfoBuilder.GetLocalTimestamp(); var fromScopeLocalTimeStamp = message.FromScope.Timestamp; changeApplicationAction = syncAdapter.HandleConflict(conflict, message.Policy, message.FromScope, fromScopeLocalTimeStamp, out DmRow resolvedRow); if (changeApplicationAction == ChangeApplicationAction.Continue) { // row resolved if (resolvedRow != null) { rowsApplied++; } } else { context.TotalSyncErrors++; // TODO : Should we break at the first error ? return(ChangeApplicationAction.Rollback); } } } // Get all conflicts resolved context.TotalSyncConflicts = conflicts.Where(c => c.Type != ConflictType.ErrorsOccurred).Sum(c => 1); // Handle sync progress for this syncadapter (so this table) var changedFailed = dmChangesView.Count - rowsApplied; // raise SyncProgress Event var existAppliedChanges = changesApplied.TableChangesApplied.FirstOrDefault( sc => string.Equals(sc.TableName, tableDescription.TableName) && sc.State == applyType); if (existAppliedChanges == null) { existAppliedChanges = new TableChangesApplied { TableName = tableDescription.TableName, Applied = rowsApplied, Failed = changedFailed, State = applyType }; changesApplied.TableChangesApplied.Add(existAppliedChanges); } else { existAppliedChanges.Applied += rowsApplied; existAppliedChanges.Failed += changedFailed; } // Event progress context.SyncStage = SyncStage.TableChangesApplied; var progressEventArgs = new TableChangesAppliedEventArgs(this.ProviderTypeName, context.SyncStage, existAppliedChanges); this.TryRaiseProgressEvent(progressEventArgs, this.TableChangesApplied); } } // Dispose conflict handler resolver if (syncAdapter.ConflictActionInvoker != null) { syncAdapter.ConflictActionInvoker = null; } } return(ChangeApplicationAction.Continue); }
/// <summary> /// Launch a synchronization with the specified mode /// </summary> public async Task <SyncContext> SynchronizeAsync(SyncType syncType, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(this.scopeName)) { throw new ArgumentNullException("scopeName", "Scope Name is mandatory"); } // Context, used to back and forth data between servers SyncContext context = new SyncContext(Guid.NewGuid()) { // set start time StartTime = DateTime.Now, // if any parameters, set in context Parameters = this.Parameters, // set sync type (Normal, Reinitialize, ReinitializeWithUpload) SyncType = syncType }; this.SessionState = SyncSessionState.Synchronizing; this.SessionStateChanged?.Invoke(this, this.SessionState); ScopeInfo localScopeInfo = null, serverScopeInfo = null, localScopeReferenceInfo = null, scope = null; Guid fromId = Guid.Empty; long lastSyncTS = 0L; bool isNew = true; try { if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Setting the cancellation token this.LocalProvider.SetCancellationToken(cancellationToken); this.RemoteProvider.SetCancellationToken(cancellationToken); // Begin Session / Read the adapters context = await this.RemoteProvider.BeginSessionAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context = await this.LocalProvider.BeginSessionAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 1) Read scope info // ---------------------------------------- // get the scope from local provider List <ScopeInfo> localScopes; List <ScopeInfo> serverScopes; (context, localScopes) = await this.LocalProvider.EnsureScopesAsync(context, scopeName); if (localScopes.Count != 1) { throw new Exception("On Local provider, we should have only one scope info"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } localScopeInfo = localScopes[0]; (context, serverScopes) = await this.RemoteProvider.EnsureScopesAsync(context, scopeName, localScopeInfo.Id); if (serverScopes.Count != 2) { throw new Exception("On Remote provider, we should have two scopes (one for server and one for client side)"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } serverScopeInfo = serverScopes.First(s => s.Id != localScopeInfo.Id); localScopeReferenceInfo = serverScopes.First(s => s.Id == localScopeInfo.Id); // ---------------------------------------- // 2) Build Configuration Object // ---------------------------------------- // Get Configuration from remote provider (context, this.Configuration) = await this.RemoteProvider.EnsureConfigurationAsync(context, this.Configuration); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Invert policy on the client var configurationLocale = this.Configuration.Clone(); var policy = this.Configuration.ConflictResolutionPolicy; if (policy == ConflictResolutionPolicy.ServerWins) { configurationLocale.ConflictResolutionPolicy = ConflictResolutionPolicy.ClientWins; } if (policy == ConflictResolutionPolicy.ClientWins) { configurationLocale.ConflictResolutionPolicy = ConflictResolutionPolicy.ServerWins; } // Apply on local Provider SyncConfiguration configuration; (context, configuration) = await this.LocalProvider.EnsureConfigurationAsync(context, configurationLocale); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 3) Ensure databases are ready // ---------------------------------------- // Server should have already the schema context = await this.RemoteProvider.EnsureDatabaseAsync(context, serverScopeInfo); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Client could have, or not, the tables context = await this.LocalProvider.EnsureDatabaseAsync(context, localScopeInfo); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 5) Get changes and apply them // ---------------------------------------- BatchInfo clientBatchInfo; BatchInfo serverBatchInfo; ChangesSelected clientChangesSelected = null; ChangesSelected serverChangesSelected = null; ChangesApplied clientChangesApplied = null; ChangesApplied serverChangesApplied = null; // fromId : not really needed on this case, since updated / inserted / deleted row has marked null // otherwise, lines updated by server or others clients are already syncked fromId = localScopeInfo.Id; // lastSyncTS : get lines inserted / updated / deteleted after the last sync commited lastSyncTS = localScopeInfo.LastTimestamp; // isNew : If isNew, lasttimestamp is not correct, so grab all isNew = localScopeInfo.IsNewScope; //Direction set to Upload context.SyncWay = SyncWay.Upload; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, clientBatchInfo, clientChangesSelected) = await this.LocalProvider.GetChangeBatchAsync(context, scope); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply on the Server Side // Since we are on the server, // we need to check the server client timestamp (not the client timestamp which is completely different) // fromId : When applying rows, make sure it's identified as applied by this client scope fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastTimestamp; // isNew : not needed isNew = false; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, serverChangesApplied) = await this.RemoteProvider.ApplyChangesAsync(context, scope, clientBatchInfo); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Get changes from server // get the archive if exists if (localScopeReferenceInfo.IsNewScope && !String.IsNullOrEmpty(this.Configuration.Archive)) { //// fromId : Make sure we don't select lines on server that has been already updated by the client //fromId = localScopeInfo.Id; //// lastSyncTS : apply lines only if thye are not modified since last client sync //lastSyncTS = localScopeReferenceInfo.LastTimestamp; //// isNew : make sure we take all lines if it's the first time we get //isNew = localScopeReferenceInfo.IsNewScope; //scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; ////Direction set to Download //context.SyncWay = SyncWay.Download; //(context, serverBatchInfo, serverChangesSelected) = await this.RemoteProvider.GetArchiveAsync(context, scope); //// fromId : When applying rows, make sure it's identified as applied by this server scope //fromId = serverScopeInfo.Id; //// lastSyncTS : apply lines only if they are not modified since last client sync //lastSyncTS = localScopeInfo.LastTimestamp; //// isNew : if IsNew, don't apply deleted rows from server //isNew = localScopeInfo.IsNewScope; //scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; //(context, clientChangesApplied) = await this.LocalProvider.ApplyArchiveAsync(context, scope, serverBatchInfo); //// Here we have to change the localScopeInfo.LastTimestamp to the good one //// last ts from archive //localScopeReferenceInfo.LastTimestamp = [something from the archive]; //// we are not new anymore //localScopeReferenceInfo.IsNewScope = false; } // fromId : Make sure we don't select lines on server that has been already updated by the client fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastTimestamp; // isNew : make sure we take all lines if it's the first time we get isNew = localScopeReferenceInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; //Direction set to Download context.SyncWay = SyncWay.Download; (context, serverBatchInfo, serverChangesSelected) = await this.RemoteProvider.GetChangeBatchAsync(context, scope); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply local changes // fromId : When applying rows, make sure it's identified as applied by this server scope fromId = serverScopeInfo.Id; // lastSyncTS : apply lines only if they are not modified since last client sync lastSyncTS = localScopeInfo.LastTimestamp; // isNew : if IsNew, don't apply deleted rows from server isNew = localScopeInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, clientChangesApplied) = await this.LocalProvider.ApplyChangesAsync(context, scope, serverBatchInfo); context.TotalChangesDownloaded = clientChangesApplied.TotalAppliedChanges; context.TotalChangesUploaded = clientChangesSelected.TotalChangesSelected; context.TotalSyncErrors = clientChangesApplied.TotalAppliedChangesFailed; long serverTimestamp, clientTimestamp; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } (context, serverTimestamp) = await this.RemoteProvider.GetLocalTimestampAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } (context, clientTimestamp) = await this.LocalProvider.GetLocalTimestampAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context.CompleteTime = DateTime.Now; serverScopeInfo.IsNewScope = false; localScopeReferenceInfo.IsNewScope = false; localScopeInfo.IsNewScope = false; serverScopeInfo.LastSync = context.CompleteTime; localScopeReferenceInfo.LastSync = context.CompleteTime; localScopeInfo.LastSync = context.CompleteTime; serverScopeInfo.IsLocal = true; localScopeReferenceInfo.IsLocal = false; context = await this.RemoteProvider.WriteScopesAsync(context, new List <ScopeInfo> { serverScopeInfo, localScopeReferenceInfo }); serverScopeInfo.IsLocal = false; localScopeInfo.IsLocal = true; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context = await this.LocalProvider.WriteScopesAsync(context, new List <ScopeInfo> { localScopeInfo, serverScopeInfo }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } } catch (SyncException se) { Debug.WriteLine($"Sync Exception: {se.Message}. Type:{se.Type}. On provider: {se.ProviderName}."); throw; } catch (Exception ex) { Debug.WriteLine($"Unknwon Exception: {ex.Message}."); throw new SyncException(ex, SyncStage.None, string.Empty); } finally { // End the current session context = await this.RemoteProvider.EndSessionAsync(context); context = await this.LocalProvider.EndSessionAsync(context); this.SessionState = SyncSessionState.Ready; this.SessionStateChanged?.Invoke(this, this.SessionState); } return(context); }
/// <summary> /// Launch a synchronization with the specified mode /// </summary> public async Task <SyncContext> SynchronizeAsync(SyncType syncType, CancellationToken cancellationToken) { // Context, used to back and forth data between servers var context = new SyncContext(Guid.NewGuid()) { // set start time StartTime = DateTime.Now, // if any parameters, set in context Parameters = this.Parameters, // set sync type (Normal, Reinitialize, ReinitializeWithUpload) SyncType = syncType }; this.SessionState = SyncSessionState.Synchronizing; this.SessionStateChanged?.Invoke(this, this.SessionState); ScopeInfo localScopeInfo = null, serverScopeInfo = null, localScopeReferenceInfo = null, scope = null; var fromId = Guid.Empty; var lastSyncTS = 0L; var isNew = true; try { if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Setting the cancellation token this.LocalProvider.SetCancellationToken(cancellationToken); this.RemoteProvider.SetCancellationToken(cancellationToken); // ---------------------------------------- // 0) Begin Session / Get the Configuration from remote provider // If the configuration object is provided by the client, the server will be updated with it. // ---------------------------------------- (context, this.Configuration) = await this.RemoteProvider.BeginSessionAsync(context, new MessageBeginSession { SyncConfiguration = this.Configuration }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Locally, nothing really special. Eventually, editing the config object (context, this.Configuration) = await this.LocalProvider.BeginSessionAsync(context, new MessageBeginSession { SyncConfiguration = this.Configuration }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 1) Read scope info // ---------------------------------------- // get the scope from local provider List <ScopeInfo> localScopes; List <ScopeInfo> serverScopes; (context, localScopes) = await this.LocalProvider.EnsureScopesAsync(context, new MessageEnsureScopes { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, ScopeName = this.Configuration.ScopeName, SerializationFormat = this.Configuration.SerializationFormat }); if (localScopes.Count != 1) { throw new Exception("On Local provider, we should have only one scope info"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } localScopeInfo = localScopes[0]; (context, serverScopes) = await this.RemoteProvider.EnsureScopesAsync(context, new MessageEnsureScopes { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, ScopeName = this.Configuration.ScopeName, ClientReferenceId = localScopeInfo.Id, SerializationFormat = this.Configuration.SerializationFormat }); if (serverScopes.Count != 2) { throw new Exception("On Remote provider, we should have two scopes (one for server and one for client side)"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } serverScopeInfo = serverScopes.First(s => s.Id != localScopeInfo.Id); localScopeReferenceInfo = serverScopes.First(s => s.Id == localScopeInfo.Id); // ---------------------------------------- // 2) Build Configuration Object // ---------------------------------------- // Get Schema from remote provider (context, this.Configuration.Schema) = await this.RemoteProvider.EnsureSchemaAsync(context, new MessageEnsureSchema { Schema = this.Configuration.Schema, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply on local Provider (context, this.Configuration.Schema) = await this.LocalProvider.EnsureSchemaAsync(context, new MessageEnsureSchema { Schema = this.Configuration.Schema, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 3) Ensure databases are ready // ---------------------------------------- // Server should have already the schema context = await this.RemoteProvider.EnsureDatabaseAsync(context, new MessageEnsureDatabase { ScopeInfo = serverScopeInfo, Schema = this.Configuration.Schema, Filters = this.Configuration.Filters, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Client could have, or not, the tables context = await this.LocalProvider.EnsureDatabaseAsync(context, new MessageEnsureDatabase { ScopeInfo = localScopeInfo, Schema = this.Configuration.Schema, Filters = this.Configuration.Filters, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 5) Get changes and apply them // ---------------------------------------- BatchInfo clientBatchInfo; BatchInfo serverBatchInfo; ChangesSelected clientChangesSelected = null; ChangesSelected serverChangesSelected = null; ChangesApplied clientChangesApplied = null; ChangesApplied serverChangesApplied = null; // those timestamps will be registered as the "timestamp just before launch the sync" long serverTimestamp, clientTimestamp; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply on the Server Side // Since we are on the server, // we need to check the server client timestamp (not the client timestamp which is completely different) var serverPolicy = this.Configuration.ConflictResolutionPolicy; var clientPolicy = serverPolicy == ConflictResolutionPolicy.ServerWins ? ConflictResolutionPolicy.ClientWins : ConflictResolutionPolicy.ServerWins; // We get from local provider all rows not last updated from the server fromId = serverScopeInfo.Id; // lastSyncTS : get lines inserted / updated / deteleted after the last sync commited lastSyncTS = localScopeInfo.LastSyncTimestamp; // isNew : If isNew, lasttimestamp is not correct, so grab all isNew = localScopeInfo.IsNewScope; //Direction set to Upload context.SyncWay = SyncWay.Upload; // JUST before the whole process, get the timestamp, to be sure to // get rows inserted / updated elsewhere since the sync is not over (context, clientTimestamp) = await this.LocalProvider.GetLocalTimestampAsync(context, new MessageTimestamp { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, Timestamp = lastSyncTS }; (context, clientBatchInfo, clientChangesSelected) = await this.LocalProvider.GetChangeBatchAsync(context, new MessageGetChangesBatch { ScopeInfo = scope, Schema = this.Configuration.Schema, DownloadBatchSizeInKB = this.Configuration.DownloadBatchSizeInKB, BatchDirectory = this.Configuration.BatchDirectory, Policy = clientPolicy, Filters = this.Configuration.Filters, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // fromId : When applying rows, make sure it's identified as applied by this client scope fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastSyncTimestamp; // isNew : not needed isNew = false; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, Timestamp = lastSyncTS }; (context, serverChangesApplied) = await this.RemoteProvider.ApplyChangesAsync(context, new MessageApplyChanges { FromScope = scope, Schema = this.Configuration.Schema, Policy = serverPolicy, UseBulkOperations = this.Configuration.UseBulkOperations, CleanMetadatas = this.Configuration.CleanMetadatas, ScopeInfoTableName = this.Configuration.ScopeInfoTableName, Changes = clientBatchInfo, SerializationFormat = this.Configuration.SerializationFormat }); // if ConflictResolutionPolicy.ClientWins or Handler set to Client wins // Conflict occurs here and server loose. // Conflicts count should be temp saved because applychanges on client side won't raise any conflicts (and so property Context.TotalSyncConflicts will be reset to 0) var conflictsOnRemoteCount = context.TotalSyncConflicts; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Get changes from server // get the archive if exists if (localScopeReferenceInfo.IsNewScope && !string.IsNullOrEmpty(this.Configuration.Archive)) { //// fromId : Make sure we don't select lines on server that has been already updated by the client //fromId = localScopeInfo.Id; //// lastSyncTS : apply lines only if thye are not modified since last client sync //lastSyncTS = localScopeReferenceInfo.LastTimestamp; //// isNew : make sure we take all lines if it's the first time we get //isNew = localScopeReferenceInfo.IsNewScope; //scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; ////Direction set to Download //context.SyncWay = SyncWay.Download; //(context, serverBatchInfo, serverChangesSelected) = await this.RemoteProvider.GetArchiveAsync(context, scope); //// fromId : When applying rows, make sure it's identified as applied by this server scope //fromId = serverScopeInfo.Id; //// lastSyncTS : apply lines only if they are not modified since last client sync //lastSyncTS = localScopeInfo.LastTimestamp; //// isNew : if IsNew, don't apply deleted rows from server //isNew = localScopeInfo.IsNewScope; //scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; //(context, clientChangesApplied) = await this.LocalProvider.ApplyArchiveAsync(context, scope, serverBatchInfo); //// Here we have to change the localScopeInfo.LastTimestamp to the good one //// last ts from archive //localScopeReferenceInfo.LastTimestamp = [something from the archive]; //// we are not new anymore //localScopeReferenceInfo.IsNewScope = false; } // fromId : Make sure we don't select lines on server that has been already updated by the client fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastSyncTimestamp; // isNew : make sure we take all lines if it's the first time we get isNew = localScopeReferenceInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, Timestamp = lastSyncTS }; //Direction set to Download context.SyncWay = SyncWay.Download; // JUST Before get changes, get the timestamp, to be sure to // get rows inserted / updated elsewhere since the sync is not over (context, serverTimestamp) = await this.RemoteProvider.GetLocalTimestampAsync(context, new MessageTimestamp { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } (context, serverBatchInfo, serverChangesSelected) = await this.RemoteProvider.GetChangeBatchAsync(context, new MessageGetChangesBatch { ScopeInfo = scope, Schema = this.Configuration.Schema, DownloadBatchSizeInKB = this.Configuration.DownloadBatchSizeInKB, BatchDirectory = this.Configuration.BatchDirectory, Policy = serverPolicy, Filters = this.Configuration.Filters, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply local changes // fromId : When applying rows, make sure it's identified as applied by this server scope fromId = serverScopeInfo.Id; // lastSyncTS : apply lines only if they are not modified since last client sync lastSyncTS = localScopeInfo.LastSyncTimestamp; // isNew : if IsNew, don't apply deleted rows from server isNew = localScopeInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, Timestamp = lastSyncTS }; (context, clientChangesApplied) = await this.LocalProvider.ApplyChangesAsync(context, new MessageApplyChanges { FromScope = scope, Schema = this.Configuration.Schema, Policy = clientPolicy, UseBulkOperations = this.Configuration.UseBulkOperations, CleanMetadatas = this.Configuration.CleanMetadatas, ScopeInfoTableName = this.Configuration.ScopeInfoTableName, Changes = serverBatchInfo, SerializationFormat = this.Configuration.SerializationFormat }); context.TotalChangesDownloaded = clientChangesApplied.TotalAppliedChanges; context.TotalChangesUploaded = clientChangesSelected.TotalChangesSelected; context.TotalSyncErrors = clientChangesApplied.TotalAppliedChangesFailed; context.CompleteTime = DateTime.Now; serverScopeInfo.IsNewScope = false; localScopeReferenceInfo.IsNewScope = false; localScopeInfo.IsNewScope = false; serverScopeInfo.LastSync = context.CompleteTime; localScopeReferenceInfo.LastSync = context.CompleteTime; localScopeInfo.LastSync = context.CompleteTime; serverScopeInfo.LastSyncTimestamp = serverTimestamp; localScopeReferenceInfo.LastSyncTimestamp = serverTimestamp; localScopeInfo.LastSyncTimestamp = clientTimestamp; var duration = context.CompleteTime.Subtract(context.StartTime); serverScopeInfo.LastSyncDuration = duration.Ticks; localScopeReferenceInfo.LastSyncDuration = duration.Ticks; localScopeInfo.LastSyncDuration = duration.Ticks; serverScopeInfo.IsLocal = true; localScopeReferenceInfo.IsLocal = false; context = await this.RemoteProvider.WriteScopesAsync(context, new MessageWriteScopes { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, Scopes = new List <ScopeInfo> { serverScopeInfo, localScopeReferenceInfo }, SerializationFormat = this.Configuration.SerializationFormat }); serverScopeInfo.IsLocal = false; localScopeInfo.IsLocal = true; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context = await this.LocalProvider.WriteScopesAsync(context, new MessageWriteScopes { ScopeInfoTableName = this.Configuration.ScopeInfoTableName, Scopes = new List <ScopeInfo> { localScopeInfo, serverScopeInfo }, SerializationFormat = this.Configuration.SerializationFormat }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } } catch (SyncException se) { Console.WriteLine($"Sync Exception: {se.Message}. Type:{se.Type}. On provider: {se.ProviderName}."); throw; } catch (Exception ex) { Console.WriteLine($"Unknwon Exception: {ex.Message}."); throw new SyncException(ex, SyncStage.None, string.Empty); } finally { // End the current session context = await this.RemoteProvider.EndSessionAsync(context); context = await this.LocalProvider.EndSessionAsync(context); this.SessionState = SyncSessionState.Ready; this.SessionStateChanged?.Invoke(this, this.SessionState); } return(context); }
/// <summary> /// Apply changes : Insert / Updates Delete /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, ChangesApplied)> ApplyChangesAsync(SyncContext context, ScopeInfo fromScope, BatchInfo changes) { try { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; ChangesApplied changesApplied = new ChangesApplied(); using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); Debug.WriteLine($"----- Applying Changes for Scope \"{fromScope.Name}\" -----"); Debug.WriteLine(""); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, connection, applyTransaction, fromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!fromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } // ----------------------------------------------------- // 1) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } applyTransaction.Commit(); Debug.WriteLine($"--- End Applying Changes for Scope \"{fromScope.Name}\" ---"); Debug.WriteLine(""); } catch (Exception exception) { Debug.WriteLine($"Caught exception while applying changes: {exception}"); throw; } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (changes != null) { changes.Clear(); } } return(context, changesApplied); } } catch (Exception) { throw; } }