Esempio n. 1
0
 public DatabaseChangesAppliedArgs(SyncContext context, DatabaseChangesApplied changesApplied, DbConnection connection, DbTransaction transaction)
     : base(context, connection, transaction)
 {
     this.ChangesApplied = changesApplied;
 }
Esempio n. 2
0
        /// <summary>
        /// Apply changes : Delete / Insert / Update
        /// the fromScope is local client scope when this method is called from server
        /// the fromScope is server scope when this method is called from client
        /// </summary>
        public virtual async Task <(SyncContext, DatabaseChangesApplied)> ApplyChangesAsync(SyncContext context, MessageApplyChanges message)
        {
            var           changeApplicationAction = ChangeApplicationAction.Continue;
            DbTransaction applyTransaction        = null;
            DbConnection  connection     = null;
            var           changesApplied = new DatabaseChangesApplied();

            try
            {
                using (connection = this.CreateConnection())
                {
                    await connection.OpenAsync();

                    await this.InterceptAsync(new ConnectionOpenArgs(context, connection));

                    // Create a transaction
                    using (applyTransaction = connection.BeginTransaction())
                    {
                        await this.InterceptAsync(new TransactionOpenArgs(context, connection, applyTransaction));

                        context.SyncStage = SyncStage.DatabaseChangesApplying;

                        // Launch any interceptor if available
                        await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, connection, applyTransaction));

                        // Disable check constraints
                        if (this.Options.DisableConstraintsOnApplyChanges)
                        {
                            changeApplicationAction = this.DisableConstraints(context, message.Schema, connection, applyTransaction, message.FromScope);
                        }

                        var tables = message.Schema.Tables.SortByDependencies(tab => tab.ParentRelations
                                                                              .Select(r => r.ParentTable))
                                     .ToArray();
                        // -----------------------------------------------------
                        // 0) Check if we are in a reinit mode
                        // -----------------------------------------------------
                        if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal)
                        {
                            changeApplicationAction = this.ResetInternal(context, message.Schema, connection, applyTransaction, message.FromScope);

                            // Rollback
                            if (changeApplicationAction == ChangeApplicationAction.Rollback)
                            {
                                throw new SyncException("Rollback during reset tables", context.SyncStage, SyncExceptionType.Rollback);
                            }
                        }

                        // -----------------------------------------------------
                        // 1) Applying deletes. Do not apply deletes if we are in a new database
                        // -----------------------------------------------------
                        if (!message.FromScope.IsNewScope)
                        {
                            // for delete we must go from Up to Down
                            foreach (var table in tables.Reverse())
                            {
                                changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                               applyTransaction, DmRowState.Deleted, changesApplied);
                            }

                            // Rollback
                            if (changeApplicationAction == ChangeApplicationAction.Rollback)
                            {
                                RaiseRollbackException(context, "Rollback during applying deletes");
                            }
                        }

                        // -----------------------------------------------------
                        // 2) Applying Inserts and Updates. Apply in table order
                        // -----------------------------------------------------
                        foreach (var table in tables)
                        {
                            changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                           applyTransaction, DmRowState.Added, changesApplied);

                            // Rollback
                            if (changeApplicationAction == ChangeApplicationAction.Rollback)
                            {
                                RaiseRollbackException(context, "Rollback during applying inserts");
                            }

                            changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                           applyTransaction, DmRowState.Modified, changesApplied);

                            // Rollback
                            if (changeApplicationAction == ChangeApplicationAction.Rollback)
                            {
                                RaiseRollbackException(context, "Rollback during applying updates");
                            }
                        }


                        // Progress & Interceptor
                        context.SyncStage = SyncStage.DatabaseChangesApplied;
                        var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, connection, applyTransaction);
                        this.ReportProgress(context, databaseChangesAppliedArgs, connection, applyTransaction);
                        await this.InterceptAsync(databaseChangesAppliedArgs);

                        // Re enable check constraints
                        if (this.Options.DisableConstraintsOnApplyChanges)
                        {
                            changeApplicationAction = this.EnableConstraints(context, message.Schema, connection, applyTransaction, message.FromScope);
                        }

                        await this.InterceptAsync(new TransactionCommitArgs(context, connection, applyTransaction));

                        applyTransaction.Commit();
                    }

                    return(context, changesApplied);
                }
            }
            catch (SyncException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new SyncException(ex, SyncStage.TableChangesApplying);
            }
            finally
            {
                if (applyTransaction != null)
                {
                    applyTransaction.Dispose();
                }

                if (connection != null && connection.State == ConnectionState.Open)
                {
                    connection.Close();
                }

                if (message.Changes != null)
                {
                    message.Changes.Clear(this.Options.CleanMetadatas);
                }

                await this.InterceptAsync(new ConnectionCloseArgs(context, connection, applyTransaction));
            }
        }
Esempio n. 3
0
        /// <summary>
        /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter
        /// </summary>
        internal async Task <ChangeApplicationAction> ApplyChangesInternalAsync(
            DmTable table,
            SyncContext context,
            MessageApplyChanges message,
            DbConnection connection,
            DbTransaction transaction,
            DmRowState applyType,
            DatabaseChangesApplied changesApplied)
        {
            var changeApplicationAction = ChangeApplicationAction.Continue;

            // if we are in upload stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Upload && table.SyncDirection == SyncDirection.DownloadOnly)
            {
                return(ChangeApplicationAction.Continue);
            }

            // if we are in download stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Download && table.SyncDirection == SyncDirection.UploadOnly)
            {
                return(ChangeApplicationAction.Continue);
            }

            var builder = this.GetDatabaseBuilder(table);

            var syncAdapter = builder.CreateSyncAdapter(connection, transaction);

            syncAdapter.ApplyType = applyType;

            if (message.Changes.BatchPartsInfo != null && message.Changes.BatchPartsInfo.Count > 0)
            {
                // getting the table to be applied
                // we may have multiple batch files, so we can have multipe dmTable with the same Name
                // We can say that dmTable may be contained in several files
                foreach (var dmTablePart in message.Changes.GetTable(table.TableName))
                {
                    if (dmTablePart == null || dmTablePart.Rows.Count == 0)
                    {
                        continue;
                    }

                    // check and filter
                    var dmChangesView = new DmView(dmTablePart, (r) => r.RowState == applyType);

                    if (dmChangesView.Count == 0)
                    {
                        dmChangesView.Dispose();
                        dmChangesView = null;
                        continue;
                    }

                    // Conflicts occured when trying to apply rows
                    var conflicts = new List <SyncConflict>();

                    context.SyncStage = SyncStage.TableChangesApplying;
                    // Launch any interceptor if available
                    await this.InterceptAsync(new TableChangesApplyingArgs(context, table, applyType, connection, transaction));

                    int rowsApplied;
                    // applying the bulkchanges command
                    if (this.Options.UseBulkOperations && this.SupportBulkOperations)
                    {
                        rowsApplied = syncAdapter.ApplyBulkChanges(dmChangesView, message.FromScope, conflicts);
                    }
                    else
                    {
                        rowsApplied = syncAdapter.ApplyChanges(dmChangesView, message.FromScope, conflicts);
                    }

                    // If conflicts occured
                    // Eventuall, conflicts are resolved on server side.
                    if (conflicts != null && conflicts.Count > 0)
                    {
                        foreach (var conflict in conflicts)
                        {
                            //var scopeBuilder = this.GetScopeBuilder();
                            //var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(message.ScopeInfoTableName, connection, transaction);
                            //var localTimeStamp = scopeInfoBuilder.GetLocalTimestamp();
                            var fromScopeLocalTimeStamp = message.FromScope.Timestamp;

                            var   conflictCount = 0;
                            DmRow resolvedRow   = null;
                            (changeApplicationAction, conflictCount, resolvedRow) =
                                await this.HandleConflictAsync(syncAdapter, context, conflict, message.Policy, message.FromScope, fromScopeLocalTimeStamp, connection, transaction);

                            if (changeApplicationAction == ChangeApplicationAction.Continue)
                            {
                                // row resolved
                                if (resolvedRow != null)
                                {
                                    context.TotalSyncConflicts += conflictCount;
                                    rowsApplied++;
                                }
                            }
                            else
                            {
                                context.TotalSyncErrors++;
                                // TODO : Should we break at the first error ?
                                return(ChangeApplicationAction.Rollback);
                            }
                        }
                    }

                    // Handle sync progress for this syncadapter (so this table)
                    var changedFailed = dmChangesView.Count - rowsApplied;

                    // raise SyncProgress Event
                    var existAppliedChanges = changesApplied.TableChangesApplied.FirstOrDefault(
                        sc => string.Equals(sc.Table.TableName, table.TableName) && sc.State == applyType);

                    if (existAppliedChanges == null)
                    {
                        existAppliedChanges = new TableChangesApplied
                        {
                            Table   = new DmTableSurrogate(table),
                            Applied = rowsApplied,
                            Failed  = changedFailed,
                            State   = applyType
                        };
                        changesApplied.TableChangesApplied.Add(existAppliedChanges);
                    }
                    else
                    {
                        existAppliedChanges.Applied += rowsApplied;
                        existAppliedChanges.Failed  += changedFailed;
                    }

                    // Progress & Interceptor
                    context.SyncStage = SyncStage.TableChangesApplied;
                    var tableChangesAppliedArgs = new TableChangesAppliedArgs(context, existAppliedChanges, connection, transaction);
                    this.ReportProgress(context, tableChangesAppliedArgs, connection, transaction);
                    await this.InterceptAsync(tableChangesAppliedArgs);
                }
            }

            return(ChangeApplicationAction.Continue);
        }
Esempio n. 4
0
        ApplyChangesAsync(ScopeInfo scope, SyncSet schema, BatchInfo serverBatchInfo,
                          long clientTimestamp, long remoteClientTimestamp, ConflictResolutionPolicy policy,
                          CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null)
        {
            if (!this.StartTime.HasValue)
            {
                this.StartTime = DateTime.UtcNow;
            }

            // Get context or create a new one
            var ctx = this.GetContext();
            DatabaseChangesApplied clientChangesApplied = null;

            using (var connection = this.Provider.CreateConnection())
            {
                try
                {
                    ctx.SyncStage = SyncStage.ChangesApplying;


                    // Open connection
                    await this.OpenConnectionAsync(connection, cancellationToken).ConfigureAwait(false);

                    // Create a transaction
                    using (var transaction = connection.BeginTransaction())
                    {
                        await this.InterceptAsync(new TransactionOpenedArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                        // lastSyncTS : apply lines only if they are not modified since last client sync
                        var lastSyncTS = scope.LastSyncTimestamp;
                        // isNew : if IsNew, don't apply deleted rows from server
                        var isNew = scope.IsNewScope;
                        // We are in downloading mode
                        ctx.SyncWay = SyncWay.Download;


                        // Create the message containing everything needed to apply changes
                        var applyChanges = new MessageApplyChanges(scope.Id, Guid.Empty, isNew, lastSyncTS, schema, this.Setup, policy,
                                                                   this.Options.DisableConstraintsOnApplyChanges,
                                                                   this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.CleanFolder,
                                                                   serverBatchInfo);

                        // call interceptor
                        await this.InterceptAsync(new DatabaseChangesApplyingArgs(ctx, applyChanges, connection, transaction), cancellationToken).ConfigureAwait(false);

                        // Call apply changes on provider
                        (ctx, clientChangesApplied) = await this.Provider.ApplyChangesAsync(ctx, applyChanges, connection, transaction, cancellationToken, progress).ConfigureAwait(false);


                        if (cancellationToken.IsCancellationRequested)
                        {
                            cancellationToken.ThrowIfCancellationRequested();
                        }

                        // check if we need to delete metadatas
                        if (this.Options.CleanMetadatas && clientChangesApplied.TotalAppliedChanges > 0)
                        {
                            await this.Provider.DeleteMetadatasAsync(ctx, schema, this.Setup, lastSyncTS, connection, transaction, cancellationToken, progress);
                        }

                        // now the sync is complete, remember the time
                        this.CompleteTime = DateTime.UtcNow;

                        // generate the new scope item
                        scope.IsNewScope              = false;
                        scope.LastSync                = this.CompleteTime;
                        scope.LastSyncTimestamp       = clientTimestamp;
                        scope.LastServerSyncTimestamp = remoteClientTimestamp;
                        scope.LastSyncDuration        = this.CompleteTime.Value.Subtract(this.StartTime.Value).Ticks;
                        scope.Setup = this.Setup;

                        // Write scopes locally
                        ctx = await this.Provider.WriteClientScopeAsync(ctx, this.Options.ScopeInfoTableName, scope, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                        await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                        transaction.Commit();
                    }

                    ctx.SyncStage = SyncStage.ChangesApplied;

                    await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false);

                    this.logger.LogInformation(SyncEventsId.ApplyChanges, clientChangesApplied);

                    var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(ctx, clientChangesApplied, connection);
                    await this.InterceptAsync(databaseChangesAppliedArgs, cancellationToken).ConfigureAwait(false);

                    this.ReportProgress(ctx, progress, databaseChangesAppliedArgs);
                }
                catch (Exception ex)
                {
                    RaiseError(ex);
                }
                finally
                {
                    if (connection != null && connection.State != ConnectionState.Closed)
                    {
                        connection.Close();
                    }
                }
                return(clientChangesApplied, scope);
            }
        }
Esempio n. 5
0
 public SnapshotAppliedArgs(SyncContext context, DatabaseChangesApplied changesApplied) : base(context, null, null)
 {
     this.ChangesApplied = changesApplied;
 }
Esempio n. 6
0
        InternalApplyChangesAsync(IScopeInfo scopeInfo, SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction,
                                  CancellationToken cancellationToken, IProgress <ProgressArgs> progress)
        {
            context.SyncStage = SyncStage.ChangesApplying;
            // call interceptor
            var databaseChangesApplyingArgs = new DatabaseChangesApplyingArgs(context, message, connection, transaction);

            await this.InterceptAsync(databaseChangesApplyingArgs, progress, cancellationToken).ConfigureAwait(false);

            var changesApplied = new DatabaseChangesApplied();

            // Check if we have some data available
            var hasChanges = message.BatchInfo.HasData();

            // if we have changes or if we are in re init mode
            if (hasChanges || context.SyncType != SyncType.Normal)
            {
                var schemaTables = message.Schema.Tables.SortByDependencies(tab => tab.GetRelations().Select(r => r.GetParentTable()));

                // Disable check constraints
                // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction
                // Report this disabling constraints brefore opening a transaction
                if (message.DisableConstraintsOnApplyChanges)
                {
                    foreach (var table in schemaTables)
                    {
                        var syncAdapter = this.GetSyncAdapter(table, scopeInfo);
                        context = await this.InternalDisableConstraintsAsync(scopeInfo, context, syncAdapter, connection, transaction).ConfigureAwait(false);
                    }
                }

                // -----------------------------------------------------
                // 0) Check if we are in a reinit mode (Check also SyncWay to be sure we don't reset tables on server, then check if we don't have already applied a snapshot)
                // -----------------------------------------------------
                if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal && !message.SnapshoteApplied)
                {
                    foreach (var table in schemaTables.Reverse())
                    {
                        var syncAdapter = this.GetSyncAdapter(table, scopeInfo);
                        context = await this.InternalResetTableAsync(scopeInfo, context, syncAdapter, connection, transaction).ConfigureAwait(false);
                    }
                }

                // Trying to change order (from deletes-upserts to upserts-deletes)
                // see https://github.com/Mimetis/Dotmim.Sync/discussions/453#discussioncomment-380530

                // -----------------------------------------------------
                // 1) Applying Inserts and Updates. Apply in table order
                // -----------------------------------------------------
                if (hasChanges)
                {
                    foreach (var table in schemaTables)
                    {
                        context = await this.InternalApplyTableChangesAsync(scopeInfo, context, table, message, connection, transaction,
                                                                            DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false);
                    }
                }

                // -----------------------------------------------------
                // 2) Applying Deletes. Do not apply deletes if we are in a new database
                // -----------------------------------------------------
                if (!message.IsNew && hasChanges)
                {
                    foreach (var table in schemaTables.Reverse())
                    {
                        context = await this.InternalApplyTableChangesAsync(scopeInfo, context, table, message, connection, transaction,
                                                                            DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false);
                    }
                }

                // Re enable check constraints
                if (message.DisableConstraintsOnApplyChanges)
                {
                    foreach (var table in schemaTables)
                    {
                        context = await this.InternalEnableConstraintsAsync(scopeInfo, context, this.GetSyncAdapter(table, scopeInfo), connection, transaction).ConfigureAwait(false);
                    }
                }

                // Dispose data
                message.BatchInfo.Clear(false);
            }

            // Before cleaning, check if we are not applying changes from a snapshotdirectory
            var cleanFolder = message.CleanFolder;

            if (cleanFolder)
            {
                cleanFolder = await this.InternalCanCleanFolderAsync(scopeInfo.Name, context.Parameters, message.BatchInfo, cancellationToken, progress).ConfigureAwait(false);
            }

            // clear the changes because we don't need them anymore
            message.BatchInfo.Clear(cleanFolder);

            var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction);

            await this.InterceptAsync(databaseChangesAppliedArgs, progress, cancellationToken).ConfigureAwait(false);

            return(context, changesApplied);
        }
        ApplyThenGetChangesAsync(ScopeInfo clientScope, BatchInfo clientBatchInfo,
                                 CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null)
        {
            if (!this.StartTime.HasValue)
            {
                this.StartTime = DateTime.UtcNow;
            }

            // Get context or create a new one
            var ctx = this.GetContext();

            long remoteClientTimestamp = 0L;
            DatabaseChangesSelected serverChangesSelected = null;
            DatabaseChangesApplied  clientChangesApplied  = null;
            BatchInfo serverBatchInfo = null;
            SyncSet   schema          = null;

            using var connection = this.Provider.CreateConnection();

            try
            {
                ctx.SyncStage = SyncStage.ChangesApplying;

                //Direction set to Upload
                ctx.SyncWay = SyncWay.Upload;

                // Open connection
                await this.OpenConnectionAsync(connection, cancellationToken).ConfigureAwait(false);

                DbTransaction transaction;

                // Create two transactions
                // First one to commit changes
                // Second one to get changes now that everything is commited
                using (transaction = connection.BeginTransaction())
                {
                    await this.InterceptAsync(new TransactionOpenedArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                    // Maybe here, get the schema from server, issue from client scope name
                    // Maybe then compare the schema version from client scope with schema version issued from server
                    // Maybe if different, raise an error ?
                    // Get scope if exists

                    // Getting server scope assumes we have already created the schema on server

                    var scopeBuilder = this.GetScopeBuilder(this.Options.ScopeInfoTableName);

                    var serverScopeInfo = await this.InternalGetScopeAsync <ServerScopeInfo>(ctx, DbScopeType.Server, clientScope.Name, scopeBuilder, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                    // Should we ?
                    if (serverScopeInfo.Schema == null)
                    {
                        throw new MissingRemoteOrchestratorSchemaException();
                    }

                    // Deserialiaze schema
                    schema = serverScopeInfo.Schema;

                    // Create message containing everything we need to apply on server side
                    var applyChanges = new MessageApplyChanges(Guid.Empty, clientScope.Id, false, clientScope.LastServerSyncTimestamp, schema, this.Setup, this.Options.ConflictResolutionPolicy,
                                                               this.Options.DisableConstraintsOnApplyChanges, this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.CleanFolder, clientBatchInfo);

                    // Call provider to apply changes
                    (ctx, clientChangesApplied) = await this.InternalApplyChangesAsync(ctx, applyChanges, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                    await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                    // commit first transaction
                    transaction.Commit();
                }

                ctx.SyncStage          = SyncStage.ChangesSelecting;
                ctx.ProgressPercentage = 0.55;


                using (transaction = connection.BeginTransaction())
                {
                    await this.InterceptAsync(new TransactionOpenedArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                    //Direction set to Download
                    ctx.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
                    remoteClientTimestamp = await this.InternalGetLocalTimestampAsync(ctx, connection, transaction, cancellationToken, progress);

                    // Get if we need to get all rows from the datasource
                    var fromScratch = clientScope.IsNewScope || ctx.SyncType == SyncType.Reinitialize || ctx.SyncType == SyncType.ReinitializeWithUpload;

                    var message = new MessageGetChangesBatch(clientScope.Id, Guid.Empty, fromScratch, clientScope.LastServerSyncTimestamp, schema, this.Setup, this.Options.BatchSize, this.Options.BatchDirectory);

                    // Call interceptor
                    await this.InterceptAsync(new DatabaseChangesSelectingArgs(ctx, message, connection, transaction), cancellationToken).ConfigureAwait(false);

                    // When we get the chnages from server, we create the batches if it's requested by the client
                    // the batch decision comes from batchsize from client
                    (ctx, serverBatchInfo, serverChangesSelected) =
                        await this.InternalGetChangesAsync(ctx, message, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                    if (cancellationToken.IsCancellationRequested)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                    }

                    // generate the new scope item
                    this.CompleteTime = DateTime.UtcNow;

                    var scopeHistory = new ServerHistoryScopeInfo
                    {
                        Id   = clientScope.Id,
                        Name = clientScope.Name,
                        LastSyncTimestamp = remoteClientTimestamp,
                        LastSync          = this.CompleteTime,
                        LastSyncDuration  = this.CompleteTime.Value.Subtract(this.StartTime.Value).Ticks,
                    };

                    // Write scopes locally
                    var scopeBuilder = this.GetScopeBuilder(this.Options.ScopeInfoTableName);

                    await this.InternalSaveScopeAsync(ctx, DbScopeType.ServerHistory, scopeHistory, scopeBuilder, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                    // Commit second transaction for getting changes
                    await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false);

                    transaction.Commit();
                }

                // Event progress & interceptor
                await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                RaiseError(ex);
            }
            finally
            {
                await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false);
            }
            return(remoteClientTimestamp, serverBatchInfo, this.Options.ConflictResolutionPolicy, clientChangesApplied, serverChangesSelected);
        }
Esempio n. 8
0
        InternalApplyThenGetChangesAsync(ClientScopeInfo clientScope, SyncContext context, BatchInfo clientBatchInfo, DbConnection connection = default, DbTransaction transaction = default, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null)
        {
            try
            {
                if (Provider == null)
                {
                    throw new MissingProviderException(nameof(InternalApplyThenGetChangesAsync));
                }

                long remoteClientTimestamp = 0L;
                DatabaseChangesSelected serverChangesSelected = null;
                DatabaseChangesApplied  clientChangesApplied  = null;
                BatchInfo serverBatchInfo = null;

                // Create two transactions
                // First one to commit changes
                // Second one to get changes now that everything is commited
                await using (var runner = await this.GetConnectionAsync(context, SyncMode.Writing, SyncStage.ChangesApplying, connection, transaction, cancellationToken, progress).ConfigureAwait(false))
                {
                    //Direction set to Upload
                    context.SyncWay = SyncWay.Upload;

                    IScopeInfo serverClientScopeInfo;
                    // Getting server scope assumes we have already created the schema on server
                    // Scope name is the scope name coming from client
                    // Since server can have multiples scopes
                    (context, serverClientScopeInfo) = await this.InternalLoadServerScopeInfoAsync(context, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false);

                    // Should we ?
                    if (serverClientScopeInfo == null || serverClientScopeInfo.Schema == null)
                    {
                        throw new MissingRemoteOrchestratorSchemaException();
                    }

                    // Deserialiaze schema
                    var schema = serverClientScopeInfo.Schema;

                    // Create message containing everything we need to apply on server side
                    var applyChanges = new MessageApplyChanges(Guid.Empty, clientScope.Id, false, clientScope.LastServerSyncTimestamp, schema, this.Options.ConflictResolutionPolicy,
                                                               this.Options.DisableConstraintsOnApplyChanges, this.Options.CleanMetadatas, this.Options.CleanFolder, false, clientBatchInfo);

                    // Call provider to apply changes
                    (context, clientChangesApplied) = await this.InternalApplyChangesAsync(serverClientScopeInfo, context, applyChanges, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false);

                    await this.InterceptAsync(new TransactionCommitArgs(context, runner.Connection, runner.Transaction), runner.Progress, runner.CancellationToken).ConfigureAwait(false);

                    // commit first transaction
                    await runner.CommitAsync().ConfigureAwait(false);
                }

                await using (var runner = await this.GetConnectionAsync(context, SyncMode.Reading, SyncStage.ChangesSelecting, connection, transaction, cancellationToken, progress).ConfigureAwait(false))
                {
                    context.ProgressPercentage = 0.55;


                    //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, remoteClientTimestamp) = await this.InternalGetLocalTimestampAsync(context, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress);

                    // Get if we need to get all rows from the datasource
                    var fromScratch = clientScope.IsNewScope || context.SyncType == SyncType.Reinitialize || context.SyncType == SyncType.ReinitializeWithUpload;

                    // When we get the chnages from server, we create the batches if it's requested by the client
                    // the batch decision comes from batchsize from client
                    (context, serverBatchInfo, serverChangesSelected) =
                        await this.InternalGetChangesAsync(clientScope, context, fromScratch, clientScope.LastServerSyncTimestamp, remoteClientTimestamp, clientScope.Id,
                                                           this.Provider.SupportsMultipleActiveResultSets,
                                                           this.Options.BatchDirectory, null, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false);

                    if (runner.CancellationToken.IsCancellationRequested)
                    {
                        runner.CancellationToken.ThrowIfCancellationRequested();
                    }

                    // generate the new scope item
                    this.CompleteTime = DateTime.UtcNow;

                    var scopeHistory = new ServerHistoryScopeInfo
                    {
                        Id   = clientScope.Id,
                        Name = clientScope.Name,
                        LastSyncTimestamp = remoteClientTimestamp,
                        LastSync          = this.CompleteTime,
                        LastSyncDuration  = this.CompleteTime.Value.Subtract(context.StartTime).Ticks,
                    };

                    // Write scopes locally
                    await this.InternalSaveServerHistoryScopeAsync(scopeHistory, context, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false);

                    // Commit second transaction for getting changes
                    await this.InterceptAsync(new TransactionCommitArgs(context, runner.Connection, runner.Transaction), runner.Progress, runner.CancellationToken).ConfigureAwait(false);

                    await runner.CommitAsync().ConfigureAwait(false);
                }

                var serverSyncChanges = new ServerSyncChanges(remoteClientTimestamp, serverBatchInfo, serverChangesSelected);

                return(context, serverSyncChanges, clientChangesApplied, this.Options.ConflictResolutionPolicy);
            }
            catch (Exception ex)
            {
                throw GetSyncError(context, ex);
            }
        }
Esempio n. 9
0
        /// <summary>
        /// Apply changes internal method for one type of query: Insert, Update or Delete for every batch from a table
        /// </summary>
        private async Task <SyncContext> InternalApplyTableChangesAsync(IScopeInfo scopeInfo, SyncContext context, SyncTable schemaTable, MessageApplyChanges message,
                                                                        DbConnection connection, DbTransaction transaction, DataRowState applyType, DatabaseChangesApplied changesApplied,
                                                                        CancellationToken cancellationToken, IProgress <ProgressArgs> progress)
        {
            if (this.Provider == null)
            {
                return(context);
            }

            context.SyncStage = SyncStage.ChangesApplying;

            var setupTable = scopeInfo.Setup.Tables[schemaTable.TableName, schemaTable.SchemaName];

            if (setupTable == null)
            {
                return(context);
            }

            // Only table schema is replicated, no datas are applied
            if (setupTable.SyncDirection == SyncDirection.None)
            {
                return(context);
            }

            // if we are in upload stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Upload && setupTable.SyncDirection == SyncDirection.DownloadOnly)
            {
                return(context);
            }

            // if we are in download stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Download && setupTable.SyncDirection == SyncDirection.UploadOnly)
            {
                return(context);
            }

            var hasChanges = message.BatchInfo.HasData(schemaTable.TableName, schemaTable.SchemaName);

            // Each table in the messages contains scope columns. Don't forget it
            if (!hasChanges)
            {
                return(context);
            }

            // what kind of command to execute
            var           init          = message.IsNew || context.SyncType != SyncType.Normal;
            DbCommandType dbCommandType = applyType == DataRowState.Deleted ? DbCommandType.DeleteRows : (init ? DbCommandType.InsertRows : DbCommandType.UpdateRows);

            // tmp sync table with only writable columns
            var changesSet         = schemaTable.Schema.Clone(false);
            var schemaChangesTable = DbSyncAdapter.CreateChangesTable(schemaTable, changesSet);

            // get executioning adapter
            var syncAdapter = this.GetSyncAdapter(schemaChangesTable, scopeInfo);

            syncAdapter.ApplyType = applyType;

            // Get command
            var(command, isBatch) = await syncAdapter.GetCommandAsync(dbCommandType, connection, transaction);

            if (command == null)
            {
                return(context);
            }

            var bpiTables = message.BatchInfo.GetBatchPartsInfo(schemaTable);

            // launch interceptor if any
            var args = new TableChangesApplyingArgs(context, message.BatchInfo, bpiTables, schemaTable, applyType, command, connection, transaction);

            await this.InterceptAsync(args, progress, cancellationToken).ConfigureAwait(false);

            if (args.Cancel || args.Command == null)
            {
                return(context);
            }

            command = args.Command;
            var cmdText = command.CommandText;

            TableChangesApplied tableChangesApplied = null;

            // Conflicts occured when trying to apply rows
            var conflictRows = new List <SyncRow>();

            var localSerializer = new LocalJsonSerializer();

            // If someone has an interceptor on deserializing, we read the row and intercept
            var interceptorsReading = this.interceptors.GetInterceptors <DeserializingRowArgs>();

            if (interceptorsReading.Count > 0)
            {
                localSerializer.OnReadingRow(async(schemaTable, rowString) =>
                {
                    var args = new DeserializingRowArgs(context, schemaTable, rowString);
                    await this.InterceptAsync(args, progress, cancellationToken).ConfigureAwait(false);
                    return(args.Result);
                });
            }

            // I've got all files for my table
            // applied rows for this bpi
            foreach (var batchPartInfo in bpiTables)
            {
                // Applied row for this particular BPI
                var appliedRowsTmp = 0;
                // Rows fetch (either of the good state or not) from the BPI
                var rowsFetched = 0;

                // Get full path of my batchpartinfo
                var fullPath = message.BatchInfo.GetBatchPartInfoPath(batchPartInfo).FullPath;

                // accumulating rows
                var batchRows = new List <SyncRow>();

                if (isBatch)
                {
                    foreach (var syncRow in localSerializer.ReadRowsFromFile(fullPath, schemaChangesTable))
                    {
                        rowsFetched++;

                        // Adding rows to the batch rows
                        if (batchRows.Count < this.Provider.BulkBatchMaxLinesCount)
                        {
                            if (syncRow.RowState == applyType)
                            {
                                batchRows.Add(syncRow);
                            }

                            if (rowsFetched < batchPartInfo.RowsCount && batchRows.Count < this.Provider.BulkBatchMaxLinesCount)
                            {
                                continue;
                            }
                        }
                        if (batchRows.Count <= 0)
                        {
                            continue;
                        }

                        var failedRows = schemaChangesTable.Schema.Clone().Tables[schemaChangesTable.TableName, schemaChangesTable.SchemaName];

                        command.CommandText = cmdText;
                        var batchArgs = new RowsChangesApplyingArgs(context, message.BatchInfo, batchRows, schemaChangesTable, applyType, command, connection, transaction);
                        await this.InterceptAsync(batchArgs, progress, cancellationToken).ConfigureAwait(false);

                        if (batchArgs.Cancel || batchArgs.Command == null || batchArgs.SyncRows == null || batchArgs.SyncRows.Count <= 0)
                        {
                            continue;
                        }

                        // get the correct pointer to the command from the interceptor in case user change the whole instance
                        command = batchArgs.Command;

                        await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false);

                        // execute the batch, through the provider
                        await syncAdapter.ExecuteBatchCommandAsync(command, message.SenderScopeId, batchArgs.SyncRows, schemaChangesTable, failedRows, message.LastTimestamp, connection, transaction).ConfigureAwait(false);

                        foreach (var failedRow in failedRows.Rows)
                        {
                            conflictRows.Add(failedRow);
                        }

                        //rows minus failed rows
                        appliedRowsTmp += batchRows.Count - failedRows.Rows.Count;
                        batchRows.Clear();
                    }
                }
                else
                {
                    foreach (var syncRow in localSerializer.ReadRowsFromFile(fullPath, schemaChangesTable))
                    {
                        rowsFetched++;

                        if (syncRow.RowState != applyType)
                        {
                            continue;
                        }

                        command.CommandText = cmdText;
                        var batchArgs = new RowsChangesApplyingArgs(context, message.BatchInfo, new List <SyncRow> {
                            syncRow
                        }, schemaChangesTable, applyType, command, connection, transaction);
                        await this.InterceptAsync(batchArgs, progress, cancellationToken).ConfigureAwait(false);

                        if (batchArgs.Cancel || batchArgs.Command == null || batchArgs.SyncRows == null || batchArgs.SyncRows.Count() <= 0)
                        {
                            continue;
                        }

                        // get the correct pointer to the command from the interceptor in case user change the whole instance
                        command = batchArgs.Command;

                        // Set the parameters value from row
                        syncAdapter.SetColumnParametersValues(command, batchArgs.SyncRows.First());

                        // Set the special parameters for update
                        syncAdapter.AddScopeParametersValues(command, message.SenderScopeId, message.LastTimestamp, applyType == DataRowState.Deleted, false);

                        await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false);

                        var rowAppliedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false);

                        // Check if we have a return value instead
                        var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count");

                        if (syncRowCountParam != null)
                        {
                            rowAppliedCount = (int)syncRowCountParam.Value;
                        }

                        if (rowAppliedCount > 0)
                        {
                            appliedRowsTmp++;
                        }
                        else
                        {
                            conflictRows.Add(syncRow);
                        }
                    }
                }


                // conflict rows applied
                int rowsAppliedCount = 0;
                // conflict resolved count
                int conflictsResolvedCount = 0;

                // If conflicts occured
                if (conflictRows.Count > 0)
                {
                    foreach (var conflictRow in conflictRows)
                    {
                        int     conflictResolvedCount;
                        SyncRow resolvedRow;
                        int     rowAppliedCount;
                        (context, conflictResolvedCount, resolvedRow, rowAppliedCount) =
                            await this.HandleConflictAsync(scopeInfo, context, message.LocalScopeId, message.SenderScopeId, syncAdapter, conflictRow, schemaChangesTable,
                                                           message.Policy, message.LastTimestamp, connection, transaction, cancellationToken, progress).ConfigureAwait(false);

                        conflictsResolvedCount += conflictResolvedCount;
                        rowsAppliedCount       += rowAppliedCount;
                    }

                    // add rows with resolved conflicts
                    appliedRowsTmp += rowsAppliedCount;
                }

                // Any failure ?
                var changedFailed = rowsFetched - conflictsResolvedCount - appliedRowsTmp;

                // Only Upsert DatabaseChangesApplied if we make an upsert/ delete from the batch or resolved any conflict
                if (appliedRowsTmp > 0 || conflictsResolvedCount > 0)
                {
                    // We may have multiple batch files, so we can have multipe sync tables with the same name
                    // We can say that a syncTable may be contained in several files
                    // That's why we should get an applied changes instance if already exists from a previous batch file
                    tableChangesApplied = changesApplied.TableChangesApplied.FirstOrDefault(tca =>
                    {
                        var sc = SyncGlobalization.DataSourceStringComparison;

                        var sn      = tca.SchemaName == null ? string.Empty : tca.SchemaName;
                        var otherSn = schemaTable.SchemaName == null ? string.Empty : schemaTable.SchemaName;

                        return(tca.TableName.Equals(schemaTable.TableName, sc) &&
                               sn.Equals(otherSn, sc) &&
                               tca.State == applyType);
                    });

                    if (tableChangesApplied == null)
                    {
                        tableChangesApplied = new TableChangesApplied
                        {
                            TableName         = schemaTable.TableName,
                            SchemaName        = schemaTable.SchemaName,
                            Applied           = appliedRowsTmp,
                            ResolvedConflicts = conflictsResolvedCount,
                            Failed            = changedFailed,
                            State             = applyType,
                            TotalRowsCount    = message.BatchInfo.RowsCount,
                            TotalAppliedCount = changesApplied.TotalAppliedChanges + appliedRowsTmp
                        };
                        changesApplied.TableChangesApplied.Add(tableChangesApplied);
                    }
                    else
                    {
                        tableChangesApplied.Applied           += appliedRowsTmp;
                        tableChangesApplied.TotalAppliedCount  = changesApplied.TotalAppliedChanges;
                        tableChangesApplied.ResolvedConflicts += conflictsResolvedCount;
                        tableChangesApplied.Failed            += changedFailed;
                    }

                    // we've got 0.25% to fill here
                    var progresspct = appliedRowsTmp * 0.25d / tableChangesApplied.TotalRowsCount;
                    context.ProgressPercentage += progresspct;
                }
            }

            schemaChangesTable.Dispose();
            schemaChangesTable = null;
            changesSet.Dispose();
            changesSet = null;

            // Report the overall changes applied for the current table
            if (tableChangesApplied != null)
            {
                var tableChangesAppliedArgs = new TableChangesAppliedArgs(context, tableChangesApplied, connection, transaction);
                // We don't report progress if we do not have applied any changes on the table, to limit verbosity of Progress
                await this.InterceptAsync(tableChangesAppliedArgs, progress, cancellationToken).ConfigureAwait(false);
            }


            if (command != null)
            {
                command.Dispose();
            }

            return(context);
        }
        ApplyChangesAsync(SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction,
                          CancellationToken cancellationToken, IProgress <ProgressArgs> progress)
        {
            var changesApplied = new DatabaseChangesApplied();

            // Check if we have some data available
            var hasChanges = await message.Changes.HasDataAsync(this.Orchestrator);

            if (!hasChanges)
            {
                this.Orchestrator.logger.LogDebug(SyncEventsId.ApplyChanges, changesApplied);
                return(context, changesApplied);
            }

            // Disable check constraints
            // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction
            // Report this disabling constraints brefore opening a transaction
            if (message.DisableConstraintsOnApplyChanges)
            {
                foreach (var table in message.Schema.Tables.Reverse())
                {
                    await this.DisableConstraintsAsync(context, table, message.Setup, connection, transaction);
                }
            }

            // -----------------------------------------------------
            // 0) Check if we are in a reinit mode
            // -----------------------------------------------------
            if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal)
            {
                await this.ResetInternalAsync(context, message.Schema, message.Setup, connection, transaction);
            }

            // -----------------------------------------------------
            // 1) Applying deletes. Do not apply deletes if we are in a new database
            // -----------------------------------------------------
            if (!message.IsNew)
            {
                // for delete we must go from Up to Down
                foreach (var table in message.Schema.Tables.Reverse())
                {
                    await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                         transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false);
                }
            }

            // -----------------------------------------------------
            // 2) Applying Inserts and Updates. Apply in table order
            // -----------------------------------------------------
            foreach (var table in message.Schema.Tables)
            {
                await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                     transaction, DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false);
            }

            // Re enable check constraints
            if (message.DisableConstraintsOnApplyChanges)
            {
                foreach (var table in message.Schema.Tables)
                {
                    await this.EnableConstraintsAsync(context, table, message.Setup, connection, transaction);
                }
            }


            // Before cleaning, check if we are not applying changes from a snapshotdirectory
            var cleanFolder = message.CleanFolder;

            if (cleanFolder && !String.IsNullOrEmpty(this.Options.SnapshotsDirectory) && !String.IsNullOrEmpty(message.Changes.DirectoryRoot))
            {
                var snapshotDirectory         = new DirectoryInfo(Path.Combine(this.Options.SnapshotsDirectory, context.ScopeName)).FullName;
                var messageBatchInfoDirectory = new DirectoryInfo(message.Changes.DirectoryRoot).FullName;

                // If we are getting batches from a snapshot folder, do not delete it
                if (snapshotDirectory.Equals(messageBatchInfoDirectory, SyncGlobalization.DataSourceStringComparison))
                {
                    cleanFolder = false;
                }
            }
            // clear the changes because we don't need them anymore
            message.Changes.Clear(cleanFolder);

            this.Orchestrator.logger.LogDebug(SyncEventsId.ApplyChanges, changesApplied);

            return(context, changesApplied);
        }
Esempio n. 11
0
        /// <summary>
        /// Apply changes internal method for one type of query: Insert, Update or Delete for every batch from a table
        /// </summary>
        private async Task InternalApplyTableChangesAsync(SyncContext context, SyncTable schemaTable, MessageApplyChanges message,
                                                          DbConnection connection, DbTransaction transaction, DataRowState applyType, DatabaseChangesApplied changesApplied,
                                                          CancellationToken cancellationToken, IProgress <ProgressArgs> progress)
        {
            // Only table schema is replicated, no datas are applied
            if (schemaTable.SyncDirection == SyncDirection.None)
            {
                return;
            }

            // if we are in upload stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Upload && schemaTable.SyncDirection == SyncDirection.DownloadOnly)
            {
                return;
            }

            // if we are in download stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Download && schemaTable.SyncDirection == SyncDirection.UploadOnly)
            {
                return;
            }

            var hasChanges = message.Changes.HasData(schemaTable.TableName, schemaTable.SchemaName);

            // Each table in the messages contains scope columns. Don't forget it
            if (hasChanges)
            {
                // launch interceptor if any
                var args = new TableChangesApplyingArgs(context, schemaTable, applyType, connection, transaction);
                await this.InterceptAsync(args, cancellationToken).ConfigureAwait(false);

                if (args.Cancel)
                {
                    return;
                }

                TableChangesApplied tableChangesApplied = null;

                var enumerableOfTables = message.Changes.GetTableAsync(schemaTable.TableName, schemaTable.SchemaName, this);
                var enumeratorOfTable  = enumerableOfTables.GetAsyncEnumerator();

                // getting the table to be applied
                // we may have multiple batch files, so we can have multipe sync tables with the same name
                // We can say that dmTable may be contained in several files
                while (await enumeratorOfTable.MoveNextAsync())
                {
                    var syncTable = enumeratorOfTable.Current;

                    if (syncTable == null || syncTable.Rows == null || syncTable.Rows.Count == 0)
                    {
                        continue;
                    }

                    // Creating a filtered view of my rows with the correct applyType
                    var filteredRows = syncTable.Rows.Where(r => r.RowState == applyType);

                    // no filtered rows, go next container table
                    if (filteredRows.Count() == 0)
                    {
                        continue;
                    }

                    // Create an empty Set that wil contains filtered rows to apply
                    // Need Schema for culture & case sensitive properties
                    var changesSet         = syncTable.Schema.Clone(false);
                    var schemaChangesTable = syncTable.Clone();
                    changesSet.Tables.Add(schemaChangesTable);
                    schemaChangesTable.Rows.AddRange(filteredRows.ToList());

                    // Should we use bulk operations ?
                    var usBulk = message.UseBulkOperations && this.Provider.SupportBulkOperations;

                    // Apply the changes batch
                    var(rowsApplied, conflictsResolvedCount) = await this.InternalApplyChangesBatchAsync(context, usBulk, schemaChangesTable, message, applyType, connection, transaction, cancellationToken).ConfigureAwait(false);

                    // Any failure ?
                    var changedFailed = filteredRows.Count() - conflictsResolvedCount - rowsApplied;

                    // We may have multiple batch files, so we can have multipe sync tables with the same name
                    // We can say that a syncTable may be contained in several files
                    // That's why we should get an applied changes instance if already exists from a previous batch file
                    tableChangesApplied = changesApplied.TableChangesApplied.FirstOrDefault(tca =>
                    {
                        var sc = SyncGlobalization.DataSourceStringComparison;

                        var sn      = tca.SchemaName == null ? string.Empty : tca.SchemaName;
                        var otherSn = schemaTable.SchemaName == null ? string.Empty : schemaTable.SchemaName;

                        return(tca.TableName.Equals(schemaTable.TableName, sc) &&
                               sn.Equals(otherSn, sc) &&
                               tca.State == applyType);
                    });

                    if (tableChangesApplied == null)
                    {
                        tableChangesApplied = new TableChangesApplied
                        {
                            TableName         = schemaTable.TableName,
                            SchemaName        = schemaTable.SchemaName,
                            Applied           = rowsApplied,
                            ResolvedConflicts = conflictsResolvedCount,
                            Failed            = changedFailed,
                            State             = applyType,
                            TotalRowsCount    = message.Changes.RowsCount,
                            TotalAppliedCount = changesApplied.TotalAppliedChanges + rowsApplied
                        };
                        changesApplied.TableChangesApplied.Add(tableChangesApplied);
                    }
                    else
                    {
                        tableChangesApplied.Applied           += rowsApplied;
                        tableChangesApplied.TotalAppliedCount  = changesApplied.TotalAppliedChanges;
                        tableChangesApplied.ResolvedConflicts += conflictsResolvedCount;
                        tableChangesApplied.Failed            += changedFailed;
                    }

                    // we've got 0.25% to fill here
                    var progresspct = rowsApplied * 0.25d / tableChangesApplied.TotalRowsCount;
                    context.ProgressPercentage += progresspct;

                    var tableChangesBatchAppliedArgs = new TableChangesBatchAppliedArgs(context, tableChangesApplied, connection, transaction);

                    // Report the batch changes applied
                    // We don't report progress if we do not have applied any changes on the table, to limit verbosity of Progress
                    if (tableChangesBatchAppliedArgs.TableChangesApplied.Applied > 0 || tableChangesBatchAppliedArgs.TableChangesApplied.Failed > 0 || tableChangesBatchAppliedArgs.TableChangesApplied.ResolvedConflicts > 0)
                    {
                        await this.InterceptAsync(tableChangesBatchAppliedArgs, cancellationToken).ConfigureAwait(false);

                        this.ReportProgress(context, progress, tableChangesBatchAppliedArgs, connection, transaction);
                    }
                }

                // Report the overall changes applied for the current table
                if (tableChangesApplied != null)
                {
                    var tableChangesAppliedArgs = new TableChangesAppliedArgs(context, tableChangesApplied, connection, transaction);

                    // We don't report progress if we do not have applied any changes on the table, to limit verbosity of Progress
                    if (tableChangesAppliedArgs.TableChangesApplied.Applied > 0 || tableChangesAppliedArgs.TableChangesApplied.Failed > 0 || tableChangesAppliedArgs.TableChangesApplied.ResolvedConflicts > 0)
                    {
                        await this.InterceptAsync(tableChangesAppliedArgs, cancellationToken).ConfigureAwait(false);
                    }
                }
            }
        }
        /// <summary>
        /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter
        /// </summary>
        internal async Task ApplyChangesInternalAsync(
            SyncTable schemaTable,
            SyncContext context,
            MessageApplyChanges message,
            DbConnection connection,
            DbTransaction transaction,
            DataRowState applyType,
            DatabaseChangesApplied changesApplied,
            CancellationToken cancellationToken, IProgress <ProgressArgs> progress)
        {
            this.Orchestrator.logger.LogDebug(SyncEventsId.ApplyChanges, message);

            // if we are in upload stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Upload && schemaTable.SyncDirection == SyncDirection.DownloadOnly)
            {
                return;
            }

            // if we are in download stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Download && schemaTable.SyncDirection == SyncDirection.UploadOnly)
            {
                return;
            }

            var builder = this.GetTableBuilder(schemaTable, message.Setup);

            var syncAdapter = builder.CreateSyncAdapter();

            syncAdapter.ApplyType = applyType;

            var hasChanges = await message.Changes.HasDataAsync(this.Orchestrator);

            // Each table in the messages contains scope columns. Don't forget it
            if (hasChanges)
            {
                // getting the table to be applied
                // we may have multiple batch files, so we can have multipe sync tables with the same name
                // We can say that dmTable may be contained in several files
                foreach (var syncTable in message.Changes.GetTable(schemaTable.TableName, schemaTable.SchemaName, this.Orchestrator))
                {
                    if (syncTable == null || syncTable.Rows == null || syncTable.Rows.Count == 0)
                    {
                        continue;
                    }

                    // Creating a filtered view of my rows with the correct applyType
                    var filteredRows = syncTable.Rows.Where(r => r.RowState == applyType);

                    // no filtered rows, go next container table
                    if (filteredRows.Count() == 0)
                    {
                        continue;
                    }

                    // Conflicts occured when trying to apply rows
                    var conflicts = new List <SyncConflict>();

                    // Create an empty Set that wil contains filtered rows to apply
                    // Need Schema for culture & case sensitive properties
                    var changesSet         = syncTable.Schema.Clone(false);
                    var schemaChangesTable = syncTable.Clone();
                    changesSet.Tables.Add(schemaChangesTable);
                    schemaChangesTable.Rows.AddRange(filteredRows.ToList());

                    if (this.Orchestrator.logger.IsEnabled(LogLevel.Trace))
                    {
                        foreach (var row in schemaChangesTable.Rows)
                        {
                            this.Orchestrator.logger.LogTrace(SyncEventsId.ApplyChanges, row);
                        }
                    }

                    // Launch any interceptor if available
                    await this.Orchestrator.InterceptAsync(new TableChangesApplyingArgs(context, schemaChangesTable, applyType, connection, transaction), cancellationToken).ConfigureAwait(false);

                    int rowsApplied = 0;

                    if (message.UseBulkOperations && this.SupportBulkOperations)
                    {
                        rowsApplied = await syncAdapter.ApplyBulkChangesAsync(message.LocalScopeId, message.SenderScopeId, schemaChangesTable, message.LastTimestamp, conflicts, connection, transaction);
                    }
                    else
                    {
                        rowsApplied = await syncAdapter.ApplyChangesAsync(message.LocalScopeId, message.SenderScopeId, schemaChangesTable, message.LastTimestamp, conflicts, connection, transaction);
                    }

                    // resolving conflicts
                    var(rowsAppliedCount, conflictsResolvedCount, syncErrorsCount) =
                        await ResolveConflictsAsync(context, message.LocalScopeId, message.SenderScopeId, syncAdapter, conflicts, message, connection, transaction).ConfigureAwait(false);

                    // Add conflict rows applied that are correctly resolved, as applied
                    rowsApplied += rowsAppliedCount;

                    // Handle sync progress for this syncadapter (so this table)
                    var changedFailed = filteredRows.Count() - conflictsResolvedCount - rowsApplied;

                    // We may have multiple batch files, so we can have multipe sync tables with the same name
                    // We can say that a syncTable may be contained in several files
                    // That's why we should get an applied changes instance if already exists from a previous batch file
                    var existAppliedChanges = changesApplied.TableChangesApplied.FirstOrDefault(tca =>
                    {
                        var sc = SyncGlobalization.DataSourceStringComparison;

                        var sn      = tca.SchemaName == null ? string.Empty : tca.SchemaName;
                        var otherSn = schemaTable.SchemaName == null ? string.Empty : schemaTable.SchemaName;

                        return(tca.TableName.Equals(schemaTable.TableName, sc) &&
                               sn.Equals(otherSn, sc) &&
                               tca.State == applyType);
                    });

                    if (existAppliedChanges == null)
                    {
                        existAppliedChanges = new TableChangesApplied
                        {
                            TableName         = schemaTable.TableName,
                            SchemaName        = schemaTable.SchemaName,
                            Applied           = rowsApplied,
                            ResolvedConflicts = conflictsResolvedCount,
                            Failed            = changedFailed,
                            State             = applyType
                        };
                        changesApplied.TableChangesApplied.Add(existAppliedChanges);
                    }
                    else
                    {
                        existAppliedChanges.Applied           += rowsApplied;
                        existAppliedChanges.ResolvedConflicts += conflictsResolvedCount;
                        existAppliedChanges.Failed            += changedFailed;
                    }

                    var tableChangesAppliedArgs = new TableChangesAppliedArgs(context, existAppliedChanges, connection, transaction);

                    // We don't report progress if we do not have applied any changes on the table, to limit verbosity of Progress
                    if (tableChangesAppliedArgs.TableChangesApplied.Applied > 0 || tableChangesAppliedArgs.TableChangesApplied.Failed > 0 || tableChangesAppliedArgs.TableChangesApplied.ResolvedConflicts > 0)
                    {
                        await this.Orchestrator.InterceptAsync(tableChangesAppliedArgs, cancellationToken).ConfigureAwait(false);

                        this.Orchestrator.ReportProgress(context, progress, tableChangesAppliedArgs, connection, transaction);

                        this.Orchestrator.logger.LogDebug(SyncEventsId.ApplyChanges, tableChangesAppliedArgs);
                    }
                }
            }
        }
Esempio n. 13
0
        ApplyChangesAsync(SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction,
                          CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null)
        {
            var changeApplicationAction = ChangeApplicationAction.Continue;
            var changesApplied          = new DatabaseChangesApplied();

            var hasChanges = await message.Changes.HasDataAsync();

            // Check if we have some data available
            if (!hasChanges)
            {
                return(context, changesApplied);
            }

            context.SyncStage = SyncStage.DatabaseChangesApplying;

            // Launch any interceptor if available
            await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, connection, transaction)).ConfigureAwait(false);

            // Disable check constraints
            // Because Sqlite does not support "PRAGMA foreign_keys=OFF" Inside a transaction
            // Report this disabling constraints brefore opening a transaction
            if (message.DisableConstraintsOnApplyChanges)
            {
                this.DisableConstraints(context, message.Schema, connection, transaction);
            }

            // -----------------------------------------------------
            // 0) Check if we are in a reinit mode
            // -----------------------------------------------------
            if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal)
            {
                changeApplicationAction = this.ResetInternal(context, message.Schema, connection, transaction);


                // Rollback
                if (changeApplicationAction == ChangeApplicationAction.Rollback)
                {
                    throw new RollbackException("Rollback during reset tables");
                }
            }

            // -----------------------------------------------------
            // 1) Applying deletes. Do not apply deletes if we are in a new database
            // -----------------------------------------------------
            if (!message.IsNew)
            {
                // for delete we must go from Up to Down
                foreach (var table in message.Schema.Tables.Reverse())
                {
                    changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                   transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false);
                }

                // Rollback
                if (changeApplicationAction == ChangeApplicationAction.Rollback)
                {
                    throw new RollbackException("Rollback during applying deletes");
                }
            }

            // -----------------------------------------------------
            // 2) Applying Inserts and Updates. Apply in table order
            // -----------------------------------------------------
            foreach (var table in message.Schema.Tables)
            {
                changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                               transaction, DataRowState.Modified, changesApplied, cancellationToken, progress).ConfigureAwait(false);

                // Rollback
                if (changeApplicationAction == ChangeApplicationAction.Rollback)
                {
                    throw new RollbackException("Rollback during applying updates");
                }
            }


            // Progress & Interceptor
            context.SyncStage = SyncStage.DatabaseChangesApplied;
            var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction);

            this.ReportProgress(context, progress, databaseChangesAppliedArgs, connection, transaction);
            await this.InterceptAsync(databaseChangesAppliedArgs).ConfigureAwait(false);

            // Re enable check constraints
            if (message.DisableConstraintsOnApplyChanges)
            {
                this.EnableConstraints(context, message.Schema, connection, transaction);
            }

            // clear the changes because we don't need them anymore
            message.Changes.Clear(message.CleanFolder);

            return(context, changesApplied);
        }
Esempio n. 14
0
        /// <summary>
        /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter
        /// </summary>
        internal async Task <ChangeApplicationAction> ApplyChangesInternalAsync(
            SyncTable schemaTable,
            SyncContext context,
            MessageApplyChanges message,
            DbConnection connection,
            DbTransaction transaction,
            DataRowState applyType,
            DatabaseChangesApplied changesApplied,
            CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null)
        {
            // if we are in upload stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Upload && schemaTable.SyncDirection == SyncDirection.DownloadOnly)
            {
                return(ChangeApplicationAction.Continue);
            }

            // if we are in download stage, so check if table is not download only
            if (context.SyncWay == SyncWay.Download && schemaTable.SyncDirection == SyncDirection.UploadOnly)
            {
                return(ChangeApplicationAction.Continue);
            }

            var builder = this.GetTableBuilder(schemaTable);

            var syncAdapter = builder.CreateSyncAdapter(connection, transaction);

            syncAdapter.ApplyType = applyType;

            var hasChanges = await message.Changes.HasDataAsync();

            // Each table in the messages contains scope columns. Don't forget it
            if (hasChanges)
            {
                // getting the table to be applied
                // we may have multiple batch files, so we can have multipe sync tables with the same name
                // We can say that dmTable may be contained in several files
                foreach (var syncTable in message.Changes.GetTable(schemaTable.TableName, schemaTable.SchemaName))
                {
                    if (syncTable == null || syncTable.Rows == null || syncTable.Rows.Count == 0)
                    {
                        continue;
                    }

                    // Creating a filtered view of my rows with the correct applyType
                    var filteredRows = syncTable.Rows.Where(r => r.RowState == applyType);

                    // no filtered rows, go next container table
                    if (filteredRows.Count() == 0)
                    {
                        continue;
                    }

                    // Conflicts occured when trying to apply rows
                    var conflicts = new List <SyncConflict>();

                    context.SyncStage = SyncStage.TableChangesApplying;
                    // Launch any interceptor if available
                    await this.InterceptAsync(new TableChangesApplyingArgs(context, filteredRows, schemaTable, applyType, connection, transaction)).ConfigureAwait(false);

                    // Create an empty Set that wil contains filtered rows to apply
                    // Need Schema for culture & case sensitive properties
                    var changesSet         = syncTable.Schema.Clone(false);
                    var schemaChangesTable = syncTable.Clone();
                    changesSet.Tables.Add(schemaChangesTable);
                    schemaChangesTable.Rows.AddRange(filteredRows.ToList());

                    int rowsApplied = 0;

                    if (message.UseBulkOperations && this.SupportBulkOperations)
                    {
                        rowsApplied = syncAdapter.ApplyBulkChanges(message.LocalScopeId, message.SenderScopeId, schemaChangesTable, message.LastTimestamp, conflicts);
                    }
                    else
                    {
                        rowsApplied = syncAdapter.ApplyChanges(message.LocalScopeId, message.SenderScopeId, schemaChangesTable, message.LastTimestamp, conflicts);
                    }


                    // resolving conflicts
                    (var changeApplicationAction, var conflictRowsApplied) =
                        await ResolveConflictsAsync(context, message.LocalScopeId, message.SenderScopeId, syncAdapter, conflicts, message, connection, transaction).ConfigureAwait(false);

                    if (changeApplicationAction == ChangeApplicationAction.Rollback)
                    {
                        return(ChangeApplicationAction.Rollback);
                    }

                    // Add conflict rows that are correctly resolved, as applied
                    rowsApplied += conflictRowsApplied;

                    // Handle sync progress for this syncadapter (so this table)
                    var changedFailed = filteredRows.Count() - rowsApplied;

                    // raise SyncProgress Event
                    var existAppliedChanges = changesApplied.TableChangesApplied.FirstOrDefault(
                        sc => string.Equals(sc.Table.TableName, schemaTable.TableName, SyncGlobalization.DataSourceStringComparison) && sc.State == applyType);

                    if (existAppliedChanges == null)
                    {
                        existAppliedChanges = new TableChangesApplied
                        {
                            Table   = schemaTable,
                            Applied = rowsApplied,
                            Failed  = changedFailed,
                            State   = applyType
                        };
                        changesApplied.TableChangesApplied.Add(existAppliedChanges);
                    }
                    else
                    {
                        existAppliedChanges.Applied += rowsApplied;
                        existAppliedChanges.Failed  += changedFailed;
                    }

                    // Progress & Interceptor
                    context.SyncStage = SyncStage.TableChangesApplied;
                    var tableChangesAppliedArgs = new TableChangesAppliedArgs(context, existAppliedChanges, connection, transaction);
                    this.ReportProgress(context, progress, tableChangesAppliedArgs, connection, transaction);
                    await this.InterceptAsync(tableChangesAppliedArgs).ConfigureAwait(false);
                }
            }

            return(ChangeApplicationAction.Continue);
        }
Esempio n. 15
0
        /// <summary>
        /// Launch a synchronization with the specified mode
        /// </summary>
        public async Task <SyncContext> SynchronizeAsync(SyncType syncType, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null)
        {
            // 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);

                // Setting progress
                this.LocalProvider.SetProgress(progress);

                // ----------------------------------------
                // 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.LocalProvider.Configuration) = await this.RemoteProvider.BeginSessionAsync(context,
                                                                                                          new MessageBeginSession { Configuration = this.LocalProvider.Configuration });


                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }

                // Locally, nothing really special. Eventually, editing the config object
                (context, this.LocalProvider.Configuration) = await this.LocalProvider.BeginSessionAsync(context,
                                                                                                         new MessageBeginSession { Configuration = this.LocalProvider.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.LocalProvider.Configuration.ScopeInfoTableName,
                    ScopeName           = this.LocalProvider.Configuration.ScopeName,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.ScopeInfoTableName,
                    ScopeName           = this.LocalProvider.Configuration.ScopeName,
                    ClientReferenceId   = localScopeInfo.Id,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema) = await this.RemoteProvider.EnsureSchemaAsync(context,
                                                                                                                 new MessageEnsureSchema
                {
                    Schema = this.LocalProvider.Configuration.Schema,
                    SerializationFormat = this.LocalProvider.Configuration.SerializationFormat
                });

                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }

                // Apply on local Provider
                (context, this.LocalProvider.Configuration.Schema) = await this.LocalProvider.EnsureSchemaAsync(context,
                                                                                                                new MessageEnsureSchema
                {
                    Schema = this.LocalProvider.Configuration.Schema,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema,
                    Filters             = this.LocalProvider.Configuration.Filters,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema,
                    Filters             = this.LocalProvider.Configuration.Filters,
                    SerializationFormat = this.LocalProvider.Configuration.SerializationFormat
                });

                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }

                // ----------------------------------------
                // 5) Get changes and apply them
                // ----------------------------------------
                BatchInfo clientBatchInfo;
                BatchInfo serverBatchInfo;

                DatabaseChangesSelected clientChangesSelected = null;
                DatabaseChangesSelected serverChangesSelected = null;
                DatabaseChangesApplied  clientChangesApplied  = null;
                DatabaseChangesApplied  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.LocalProvider.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.LocalProvider.Configuration.ScopeInfoTableName,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema,
                    Policy              = clientPolicy,
                    Filters             = this.LocalProvider.Configuration.Filters,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema,
                    Policy              = serverPolicy,
                    ScopeInfoTableName  = this.LocalProvider.Configuration.ScopeInfoTableName,
                    Changes             = clientBatchInfo,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.ScopeInfoTableName,
                    SerializationFormat = this.LocalProvider.Configuration.SerializationFormat
                });

                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }

                (context, serverBatchInfo, serverChangesSelected) =
                    await this.RemoteProvider.GetChangeBatchAsync(context,
                                                                  new MessageGetChangesBatch
                {
                    ScopeInfo           = scope,
                    Schema              = this.LocalProvider.Configuration.Schema,
                    Policy              = serverPolicy,
                    Filters             = this.LocalProvider.Configuration.Filters,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.Schema,
                    Policy              = clientPolicy,
                    ScopeInfoTableName  = this.LocalProvider.Configuration.ScopeInfoTableName,
                    Changes             = serverBatchInfo,
                    SerializationFormat = this.LocalProvider.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.LocalProvider.Configuration.ScopeInfoTableName,
                    Scopes             = new List <ScopeInfo> {
                        serverScopeInfo, localScopeReferenceInfo
                    },
                    SerializationFormat = this.LocalProvider.Configuration.SerializationFormat
                });


                serverScopeInfo.IsLocal = false;
                localScopeInfo.IsLocal  = true;

                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }

                context = await this.LocalProvider.WriteScopesAsync(context,
                                                                    new MessageWriteScopes
                {
                    ScopeInfoTableName = this.LocalProvider.Configuration.ScopeInfoTableName,
                    Scopes             = new List <ScopeInfo> {
                        localScopeInfo, serverScopeInfo
                    },
                    SerializationFormat = this.LocalProvider.Configuration.SerializationFormat
                });

                if (cancellationToken.IsCancellationRequested)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }
            }
            catch (SyncException se)
            {
                Console.WriteLine($"Sync Exception: {se.Message}. Type:{se.Type}.");
                throw;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Unknwon Exception: {ex.Message}.");
                throw new SyncException(ex, SyncStage.None);
            }
            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);
        }
Esempio n. 16
0
        /// <summary>
        /// Apply changes : Delete / Insert / Update
        /// the fromScope is local client scope when this method is called from server
        /// the fromScope is server scope when this method is called from client
        /// </summary>
        public virtual async Task <(SyncContext, DatabaseChangesApplied)> ApplyChangesAsync(SyncContext context, MessageApplyChanges message)
        {
            var           changeApplicationAction = ChangeApplicationAction.Continue;
            DbTransaction applyTransaction        = null;
            DbConnection  connection     = null;
            var           changesApplied = new DatabaseChangesApplied();

            try
            {
                using (connection = this.CreateConnection())
                {
                    await connection.OpenAsync();

                    // Create a transaction
                    applyTransaction = connection.BeginTransaction();

                    // -----------------------------------------------------
                    // 0) Check if we are in a reinit mode
                    // -----------------------------------------------------
                    if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal)
                    {
                        changeApplicationAction = this.ResetInternal(context, message.Schema, connection, applyTransaction, message.FromScope);

                        // Rollback
                        if (changeApplicationAction == ChangeApplicationAction.Rollback)
                        {
                            throw new SyncException("Rollback during reset tables", context.SyncStage, SyncExceptionType.Rollback);
                        }
                    }

                    // -----------------------------------------------------
                    // 1) Applying deletes. Do not apply deletes if we are in a new database
                    // -----------------------------------------------------
                    if (!message.FromScope.IsNewScope)
                    {
                        // for delete we must go from Up to Down
                        foreach (var table in message.Schema.Tables.Reverse())
                        {
                            changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                           applyTransaction, DmRowState.Deleted, changesApplied);
                        }

                        // Rollback
                        if (changeApplicationAction == ChangeApplicationAction.Rollback)
                        {
                            RaiseRollbackException(context, "Rollback during applying deletes");
                        }
                    }

                    // -----------------------------------------------------
                    // 2) Applying Inserts and Updates. Apply in table order
                    // -----------------------------------------------------
                    foreach (var table in message.Schema.Tables)
                    {
                        changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                       applyTransaction, DmRowState.Added, changesApplied);

                        // Rollback
                        if (changeApplicationAction == ChangeApplicationAction.Rollback)
                        {
                            RaiseRollbackException(context, "Rollback during applying inserts");
                        }

                        changeApplicationAction = await this.ApplyChangesInternalAsync(table, context, message, connection,
                                                                                       applyTransaction, DmRowState.Modified, changesApplied);

                        // Rollback
                        if (changeApplicationAction == ChangeApplicationAction.Rollback)
                        {
                            RaiseRollbackException(context, "Rollback during applying updates");
                        }
                    }

                    applyTransaction.Commit();


                    return(context, changesApplied);
                }
            }
            catch (SyncException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new SyncException(ex, SyncStage.TableChangesApplying);
            }
            finally
            {
                if (applyTransaction != null)
                {
                    applyTransaction.Dispose();
                    applyTransaction = null;
                }

                if (connection != null && connection.State == ConnectionState.Open)
                {
                    connection.Close();
                }

                if (message.Changes != null)
                {
                    message.Changes.Clear(this.Options.CleanMetadatas);
                }
            }
        }