/// <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();
                        }
                    }
                }
            }
        }
Example #4
0
        /// <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);
        }