/// <summary> /// Enumerate all internal changes, no batch mode /// </summary> internal async Task <(BatchInfo, ChangesSelected)> EnumerateChangesInBatchesInternal (SyncContext context, ScopeInfo scopeInfo, int downloadBatchSizeInKB, DmSet configTables, string batchDirectory, ConflictResolutionPolicy policy, ICollection <FilterClause> filters) { DmTable dmTable = null; // memory size total double memorySizeFromDmRows = 0L; int batchIndex = 0; // this batch info won't be in memory, it will be be batched BatchInfo batchInfo = new BatchInfo { // directory where all files will be stored Directory = BatchInfo.GenerateNewDirectoryName(), // not in memory since we serialized all files in the tmp directory InMemory = false }; // Create stats object to store changes count ChangesSelected changes = new ChangesSelected(); using (var connection = this.CreateConnection()) { try { // Open the connection await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { // create the in memory changes set DmSet changesSet = new DmSet(configTables.DmSetName); foreach (var tableDescription in configTables.Tables) { // 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(policy); // raise before event context.SyncStage = SyncStage.TableChangesSelecting; var beforeArgs = new TableChangesSelectingEventArgs(this.ProviderTypeName, context.SyncStage, tableDescription.TableName); this.TryRaiseProgressEvent(beforeArgs, this.TableChangesSelecting); // Get Command DbCommand selectIncrementalChangesCommand; DbCommandType dbCommandType; if (this.CanBeServerProvider && context.Parameters != null && context.Parameters.Count > 0 && filters != null && filters.Count > 0) { var filtersName = filters .Where(f => f.TableName.Equals(tableDescription.TableName, StringComparison.InvariantCultureIgnoreCase)) .Select(f => f.ColumnName); if (filtersName != null && filtersName.Count() > 0) { dbCommandType = DbCommandType.SelectChangesWitFilters; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType, filtersName); } else { dbCommandType = DbCommandType.SelectChanges; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType); } } else { dbCommandType = DbCommandType.SelectChanges; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType); } // Deriving Parameters syncAdapter.SetCommandParameters(DbCommandType.SelectChanges, selectIncrementalChangesCommand); if (selectIncrementalChangesCommand == null) { var exc = "Missing command 'SelectIncrementalChangesCommand' "; throw new Exception(exc); } dmTable = BuildChangesTable(tableDescription.TableName, configTables); try { // Set commons parameters SetSelectChangesCommonParameters(context, scopeInfo, selectIncrementalChangesCommand); // Set filter parameters if any // Only on server side if (this.CanBeServerProvider && context.Parameters != null && context.Parameters.Count > 0 && filters != null && filters.Count > 0) { var filterTable = filters.Where(f => f.TableName.Equals(tableDescription.TableName, StringComparison.InvariantCultureIgnoreCase)).ToList(); if (filterTable != null && filterTable.Count > 0) { foreach (var filter in filterTable) { var parameter = context.Parameters.FirstOrDefault(p => p.ColumnName.Equals(filter.ColumnName, StringComparison.InvariantCultureIgnoreCase) && p.TableName.Equals(filter.TableName, StringComparison.InvariantCultureIgnoreCase)); if (parameter != null) { DbManager.SetParameterValue(selectIncrementalChangesCommand, parameter.ColumnName, parameter.Value); } } } } this.AddTrackingColumns <int>(dmTable, "sync_row_is_tombstone"); // Statistics TableChangesSelected tableChangesSelected = new TableChangesSelected { TableName = tableDescription.TableName }; changes.TableChangesSelected.Add(tableChangesSelected); // Get the reader using (var dataReader = selectIncrementalChangesCommand.ExecuteReader()) { while (dataReader.Read()) { DmRow dmRow = CreateRowFromReader(dataReader, dmTable); DmRowState state = DmRowState.Unchanged; state = GetStateFromDmRow(dmRow, scopeInfo); // If the row is not deleted inserted or modified, go next if (state != DmRowState.Deleted && state != DmRowState.Modified && state != DmRowState.Added) { continue; } var fieldsSize = DmTableSurrogate.GetRowSizeFromDataRow(dmRow); var dmRowSize = fieldsSize / 1024d; if (dmRowSize > downloadBatchSizeInKB) { var exc = $"Row is too big ({dmRowSize} kb.) for the current Configuration.DownloadBatchSizeInKB ({downloadBatchSizeInKB} kb.) Aborting Sync..."; throw new Exception(exc); } // Calculate the new memory size memorySizeFromDmRows = memorySizeFromDmRows + dmRowSize; // add row dmTable.Rows.Add(dmRow); tableChangesSelected.TotalChanges++; // acceptchanges before modifying dmRow.AcceptChanges(); // Set the correct state to be applied if (state == DmRowState.Deleted) { dmRow.Delete(); tableChangesSelected.Deletes++; } else if (state == DmRowState.Added) { dmRow.SetAdded(); tableChangesSelected.Inserts++; } else if (state == DmRowState.Modified) { dmRow.SetModified(); tableChangesSelected.Updates++; } // We exceed the memorySize, so we can add it to a batch if (memorySizeFromDmRows > downloadBatchSizeInKB) { // Since we dont need this column anymore, remove it this.RemoveTrackingColumns(dmTable, "sync_row_is_tombstone"); changesSet.Tables.Add(dmTable); // generate the batch part info batchInfo.GenerateBatchInfo(batchIndex, changesSet, batchDirectory); // increment batch index batchIndex++; changesSet.Clear(); // Recreate an empty DmSet, then a dmTable clone changesSet = new DmSet(configTables.DmSetName); dmTable = dmTable.Clone(); this.AddTrackingColumns <int>(dmTable, "sync_row_is_tombstone"); // Init the row memory size memorySizeFromDmRows = 0L; // add stats for a SyncProgress event context.SyncStage = SyncStage.TableChangesSelected; var args2 = new TableChangesSelectedEventArgs (this.ProviderTypeName, SyncStage.TableChangesSelected, tableChangesSelected); this.TryRaiseProgressEvent(args2, this.TableChangesSelected); } } // Since we dont need this column anymore, remove it this.RemoveTrackingColumns(dmTable, "sync_row_is_tombstone"); context.SyncStage = SyncStage.TableChangesSelected; changesSet.Tables.Add(dmTable); // Init the row memory size memorySizeFromDmRows = 0L; // Event progress context.SyncStage = SyncStage.TableChangesSelected; var args = new TableChangesSelectedEventArgs(this.ProviderTypeName, SyncStage.TableChangesSelected, tableChangesSelected); this.TryRaiseProgressEvent(args, this.TableChangesSelected); } } catch (Exception) { throw; } finally { } } // We are in batch mode, and we are at the last batchpart info if (changesSet != null && changesSet.HasTables && changesSet.HasChanges()) { var batchPartInfo = batchInfo.GenerateBatchInfo(batchIndex, changesSet, batchDirectory); if (batchPartInfo != null) { batchPartInfo.IsLastBatch = true; } } transaction.Commit(); } } catch (Exception) { throw; } finally { if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } } } return(batchInfo, changes); }
/// <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> /// Enumerate all internal changes, no batch mode /// </summary> internal async Task <(BatchInfo, ChangesSelected)> EnumerateChangesInternal( SyncContext context, ScopeInfo scopeInfo, DmSet configTables, string batchDirectory, ConflictResolutionPolicy policy, ICollection <FilterClause> filters) { // create the in memory changes set DmSet changesSet = new DmSet(SyncConfiguration.DMSET_NAME); // Create the batch info, in memory var batchInfo = new BatchInfo { InMemory = true }; using (var connection = this.CreateConnection()) { // Open the connection await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { try { // changes that will be returned as selected changes ChangesSelected changes = new ChangesSelected(); foreach (var tableDescription in configTables.Tables) { // 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(policy); // raise before event context.SyncStage = SyncStage.TableChangesSelecting; var beforeArgs = new TableChangesSelectingEventArgs(this.ProviderTypeName, context.SyncStage, tableDescription.TableName); this.TryRaiseProgressEvent(beforeArgs, this.TableChangesSelecting); // selected changes for the current table TableChangesSelected tableSelectedChanges = new TableChangesSelected { TableName = tableDescription.TableName }; // Get Command DbCommand selectIncrementalChangesCommand; DbCommandType dbCommandType; if (this.CanBeServerProvider && context.Parameters != null && context.Parameters.Count > 0 && filters != null && filters.Count > 0) { var filtersName = filters .Where(f => f.TableName.Equals(tableDescription.TableName, StringComparison.InvariantCultureIgnoreCase)) .Select(f => f.ColumnName); if (filtersName != null && filtersName.Count() > 0) { dbCommandType = DbCommandType.SelectChangesWitFilters; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType, filtersName); } else { dbCommandType = DbCommandType.SelectChanges; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType); } } else { dbCommandType = DbCommandType.SelectChanges; selectIncrementalChangesCommand = syncAdapter.GetCommand(dbCommandType); } if (selectIncrementalChangesCommand == null) { var exc = "Missing command 'SelectIncrementalChangesCommand' "; throw new Exception(exc); } // Deriving Parameters syncAdapter.SetCommandParameters(dbCommandType, selectIncrementalChangesCommand); // Get a clone of the table with tracking columns var dmTableChanges = BuildChangesTable(tableDescription.TableName, configTables); SetSelectChangesCommonParameters(context, scopeInfo, selectIncrementalChangesCommand); // Set filter parameters if any if (this.CanBeServerProvider && context.Parameters != null && context.Parameters.Count > 0 && filters != null && filters.Count > 0) { var tableFilters = filters.Where(f => f.TableName.Equals(tableDescription.TableName, StringComparison.InvariantCultureIgnoreCase)).ToList(); if (tableFilters != null && tableFilters.Count > 0) { foreach (var filter in tableFilters) { var parameter = context.Parameters.FirstOrDefault(p => p.ColumnName.Equals(filter.ColumnName, StringComparison.InvariantCultureIgnoreCase) && p.TableName.Equals(filter.TableName, StringComparison.InvariantCultureIgnoreCase)); if (parameter != null) { DbManager.SetParameterValue(selectIncrementalChangesCommand, parameter.ColumnName, parameter.Value); } } } } this.AddTrackingColumns <int>(dmTableChanges, "sync_row_is_tombstone"); // Get the reader using (var dataReader = selectIncrementalChangesCommand.ExecuteReader()) { while (dataReader.Read()) { DmRow dataRow = CreateRowFromReader(dataReader, dmTableChanges); //DmRow dataRow = dmTableChanges.NewRow(); // assuming the row is not inserted / modified DmRowState state = DmRowState.Unchanged; // get if the current row is inserted, modified, deleted state = GetStateFromDmRow(dataRow, scopeInfo); if (state != DmRowState.Deleted && state != DmRowState.Modified && state != DmRowState.Added) { continue; } // add row dmTableChanges.Rows.Add(dataRow); // acceptchanges before modifying dataRow.AcceptChanges(); tableSelectedChanges.TotalChanges++; // Set the correct state to be applied if (state == DmRowState.Deleted) { dataRow.Delete(); tableSelectedChanges.Deletes++; } else if (state == DmRowState.Added) { dataRow.SetAdded(); tableSelectedChanges.Inserts++; } else if (state == DmRowState.Modified) { dataRow.SetModified(); tableSelectedChanges.Updates++; } } // Since we dont need this column anymore, remove it this.RemoveTrackingColumns(dmTableChanges, "sync_row_is_tombstone"); // add it to the DmSet changesSet.Tables.Add(dmTableChanges); } // add the stats to global stats changes.TableChangesSelected.Add(tableSelectedChanges); // Raise event for this table context.SyncStage = SyncStage.TableChangesSelected; var args = new TableChangesSelectedEventArgs(this.ProviderTypeName, SyncStage.TableChangesSelected, tableSelectedChanges); this.TryRaiseProgressEvent(args, this.TableChangesSelected); } transaction.Commit(); // generate the batchpartinfo batchInfo.GenerateBatchInfo(0, changesSet, batchDirectory); // Create a new in-memory batch info with an the changes DmSet return(batchInfo, changes); } catch (Exception) { throw; } finally { if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } } } } }
/// <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); }