/// <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(); 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); } // ----------------------------------------------------- // 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"); } } // 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); } 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); } } }
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; IScopeInfo serverClientScopeInfo = 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; // 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(serverClientScopeInfo, 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); } }
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, false, clientBatchInfo, this.Options.SerializerFactory); // 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, this.Options.SerializerFactory); // 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); }
/// <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) { // 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> /// Internally apply a batch changes from a table /// </summary> private async Task <(int AppliedRowsCount, int ConflictsResolvedCount)> InternalApplyChangesBatchAsync(SyncContext context, bool useBulkOperation, SyncTable changesTable, MessageApplyChanges message, DataRowState applyType, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken) { // Conflicts occured when trying to apply rows var conflictRows = new List <SyncRow>(); // get executioning adapter var syncAdapter = this.GetSyncAdapter(changesTable, message.Setup); syncAdapter.ApplyType = applyType; // Get correct command type var dbCommandType = applyType switch { DataRowState.Deleted => useBulkOperation ? DbCommandType.BulkDeleteRows : DbCommandType.DeleteRow, DataRowState.Modified => useBulkOperation ? DbCommandType.BulkUpdateRows : DbCommandType.UpdateRow, _ => throw new UnknownException("RowState not valid during ApplyBulkChanges operation"), }; // Get command var command = await syncAdapter.GetCommandAsync(dbCommandType, connection, transaction); // Launch any interceptor if available var args = new TableChangesBatchApplyingArgs(context, changesTable, applyType, command, connection, transaction); await this.InterceptAsync(args, cancellationToken).ConfigureAwait(false); if (args.Cancel || args.Command == null) { return(0, 0); } // get the correct pointer to the command from the interceptor in case user change the whole instance command = args.Command; // get the items count var itemsArrayCount = changesTable.Rows.Count; // Make some parts of BATCH_SIZE var appliedRows = await Task.Run(async() => { int appliedRowsTmp = 0; for (int step = 0; step < itemsArrayCount; step += DbSyncAdapter.BATCH_SIZE) { // get upper bound max value var taken = step + DbSyncAdapter.BATCH_SIZE >= itemsArrayCount ? itemsArrayCount - step : DbSyncAdapter.BATCH_SIZE; var arrayStepChanges = changesTable.Rows.Skip(step).Take(taken); if (useBulkOperation) { var failedPrimaryKeysTable = changesTable.Schema.Clone().Tables[changesTable.TableName, changesTable.SchemaName]; // execute the batch, through the provider await syncAdapter.ExecuteBatchCommandAsync(command, message.SenderScopeId, arrayStepChanges, changesTable, failedPrimaryKeysTable, message.LastTimestamp, connection, transaction).ConfigureAwait(false); // Get local and remote row and create the conflict object foreach (var failedRow in failedPrimaryKeysTable.Rows) { // Get the row that caused the problem, from the opposite side (usually client) var remoteConflictRow = changesTable.Rows.GetRowByPrimaryKeys(failedRow); conflictRows.Add(remoteConflictRow); } //rows minus failed rows appliedRowsTmp += taken - failedPrimaryKeysTable.Rows.Count; } else { foreach (var row in arrayStepChanges) { // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, message.SenderScopeId, message.LastTimestamp, applyType == DataRowState.Deleted, 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(row); } } } } return(appliedRowsTmp); }); // If conflicts occured if (conflictRows.Count <= 0) { return(appliedRows, 0); } // conflict rows applied int rowsAppliedCount = 0; // conflict resolved count int conflictsResolvedCount = 0; foreach (var conflictRow in conflictRows) { var fromScopeLocalTimeStamp = message.LastTimestamp; var(conflictResolvedCount, resolvedRow, rowAppliedCount) = await this.HandleConflictAsync(message.LocalScopeId, message.SenderScopeId, syncAdapter, context, conflictRow, changesTable, message.Policy, fromScopeLocalTimeStamp, connection, transaction).ConfigureAwait(false); conflictsResolvedCount += conflictResolvedCount; rowsAppliedCount += rowAppliedCount; } appliedRows += rowsAppliedCount; return(appliedRows, conflictsResolvedCount); }
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); }
/// <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); }
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); } }
/// <summary> /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter /// </summary> internal ChangeApplicationAction ApplyChangesInternal( SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, DmRowState applyType, ChangesApplied changesApplied) { ChangeApplicationAction changeApplicationAction = ChangeApplicationAction.Continue; // for each adapters (Zero to End for Insert / Updates -- End to Zero for Deletes for (int i = 0; i < message.Schema.Tables.Count; i++) { // If we have a delete we must go from Up to Down, orthewise Dow to Up index var tableDescription = (applyType != DmRowState.Deleted ? message.Schema.Tables[i] : message.Schema.Tables[message.Schema.Tables.Count - i - 1]); // if we are in upload stage, so check if table is not download only if (context.SyncWay == SyncWay.Upload && tableDescription.SyncDirection == SyncDirection.DownloadOnly) { continue; } // if we are in download stage, so check if table is not download only if (context.SyncWay == SyncWay.Download && tableDescription.SyncDirection == SyncDirection.UploadOnly) { continue; } var builder = this.GetDatabaseBuilder(tableDescription); var syncAdapter = builder.CreateSyncAdapter(connection, transaction); syncAdapter.ConflictApplyAction = SyncConfiguration.GetApplyAction(message.Policy); // Set syncAdapter properties syncAdapter.applyType = applyType; // Get conflict handler resolver if (syncAdapter.ConflictActionInvoker == null && this.ApplyChangedFailed != null) { syncAdapter.ConflictActionInvoker = GetConflictAction; } if (message.Changes.BatchPartsInfo != null && message.Changes.BatchPartsInfo.Count > 0) { // getting the table to be applied // we may have multiple batch files, so we can have multipe dmTable with the same Name // We can say that dmTable may be contained in several files foreach (DmTable dmTablePart in message.Changes.GetTable(tableDescription.TableName)) { if (dmTablePart == null || dmTablePart.Rows.Count == 0) { continue; } // check and filter var dmChangesView = new DmView(dmTablePart, (r) => r.RowState == applyType); if (dmChangesView.Count == 0) { dmChangesView.Dispose(); dmChangesView = null; continue; } // Conflicts occured when trying to apply rows List <SyncConflict> conflicts = new List <SyncConflict>(); // Raise event progress only if there are rows to be applied context.SyncStage = SyncStage.TableChangesApplying; var args = new TableChangesApplyingEventArgs(this.ProviderTypeName, context.SyncStage, tableDescription.TableName, applyType); this.TryRaiseProgressEvent(args, this.TableChangesApplying); int rowsApplied; // applying the bulkchanges command if (message.UseBulkOperations && this.SupportBulkOperations) { rowsApplied = syncAdapter.ApplyBulkChanges(dmChangesView, message.FromScope, conflicts); } else { rowsApplied = syncAdapter.ApplyChanges(dmChangesView, message.FromScope, conflicts); } // If conflicts occured // Eventuall, conflicts are resolved on server side. if (conflicts != null && conflicts.Count > 0) { foreach (var conflict in conflicts) { //var scopeBuilder = this.GetScopeBuilder(); //var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(message.ScopeInfoTableName, connection, transaction); //var localTimeStamp = scopeInfoBuilder.GetLocalTimestamp(); var fromScopeLocalTimeStamp = message.FromScope.Timestamp; changeApplicationAction = syncAdapter.HandleConflict(conflict, message.Policy, message.FromScope, fromScopeLocalTimeStamp, out DmRow resolvedRow); if (changeApplicationAction == ChangeApplicationAction.Continue) { // row resolved if (resolvedRow != null) { rowsApplied++; } } else { context.TotalSyncErrors++; // TODO : Should we break at the first error ? return(ChangeApplicationAction.Rollback); } } } // Get all conflicts resolved context.TotalSyncConflicts = conflicts.Where(c => c.Type != ConflictType.ErrorsOccurred).Sum(c => 1); // Handle sync progress for this syncadapter (so this table) var changedFailed = dmChangesView.Count - rowsApplied; // raise SyncProgress Event var existAppliedChanges = changesApplied.TableChangesApplied.FirstOrDefault( sc => string.Equals(sc.TableName, tableDescription.TableName) && sc.State == applyType); if (existAppliedChanges == null) { existAppliedChanges = new TableChangesApplied { TableName = tableDescription.TableName, Applied = rowsApplied, Failed = changedFailed, State = applyType }; changesApplied.TableChangesApplied.Add(existAppliedChanges); } else { existAppliedChanges.Applied += rowsApplied; existAppliedChanges.Failed += changedFailed; } // Event progress context.SyncStage = SyncStage.TableChangesApplied; var progressEventArgs = new TableChangesAppliedEventArgs(this.ProviderTypeName, context.SyncStage, existAppliedChanges); this.TryRaiseProgressEvent(progressEventArgs, this.TableChangesApplied); } } // Dispose conflict handler resolver if (syncAdapter.ConflictActionInvoker != null) { syncAdapter.ConflictActionInvoker = null; } } return(ChangeApplicationAction.Continue); }
/// <summary> /// 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); }
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); }
public DatabaseChangesApplyingArgs(SyncContext context, MessageApplyChanges applyChanges, DbConnection connection, DbTransaction transaction) : base(context, connection, transaction) { this.ApplyChanges = applyChanges; }
private async Task <(ChangeApplicationAction, int)> ResolveConflictsAsync(SyncContext context, Guid localScopeId, Guid senderScopeId, DbSyncAdapter syncAdapter, List <SyncConflict> conflicts, MessageApplyChanges message, DbConnection connection, DbTransaction transaction) { // If conflicts occured // Eventuall, conflicts are resolved on server side. if (conflicts == null || conflicts.Count <= 0) { return(ChangeApplicationAction.Continue, 0); } int rowsApplied = 0; foreach (var conflict in conflicts) { var fromScopeLocalTimeStamp = message.LastTimestamp; var(changeApplicationAction, conflictCount, resolvedRow, conflictApplyInt) = await this.HandleConflictAsync(localScopeId, senderScopeId, syncAdapter, context, conflict, message.Policy, fromScopeLocalTimeStamp, connection, transaction).ConfigureAwait(false); if (changeApplicationAction == ChangeApplicationAction.Continue) { if (resolvedRow != null) { context.TotalSyncConflicts += conflictCount; rowsApplied += conflictApplyInt; } } else { context.TotalSyncErrors++; return(ChangeApplicationAction.Rollback, rowsApplied); } } return(ChangeApplicationAction.Continue, rowsApplied); }
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); }
/// <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); }
/// <summary> /// Apply changes : Insert / Updates Delete /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, ChangesApplied)> ApplyChangesAsync(SyncContext context, MessageApplyChanges message) { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; DbConnection connection = null; ChangesApplied changesApplied = new ChangesApplied(); try { using (connection = this.CreateConnection()) { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, message.Schema, connection, applyTransaction, message.FromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during reset tables", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.FromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying deletes", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 2) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying updates", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } // ----------------------------------------------------- // 3) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, message, connection, applyTransaction, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying inserts", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } applyTransaction.Commit(); return(context, changesApplied); } } catch (SyncException) { throw; } catch (Exception ex) { throw new SyncException(ex, SyncStage.TableChangesApplying, this.ProviderTypeName); } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (message.Changes != null) { message.Changes.Clear(); } } }
/// <summary> /// Apply changes internal method for one Insert or Update or Delete for every dbSyncAdapter /// </summary> internal 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(connection, transaction); 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); } else { rowsApplied = await syncAdapter.ApplyChangesAsync(message.LocalScopeId, message.SenderScopeId, schemaChangesTable, message.LastTimestamp, conflicts); } // 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); } } } }
InternalApplyChangesAsync(SyncContext context, MessageApplyChanges message, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // call interceptor await this.InterceptAsync(new DatabaseChangesApplyingArgs(context, message, connection, transaction), cancellationToken).ConfigureAwait(false); var changesApplied = new DatabaseChangesApplied(); // Check if we have some data available var hasChanges = message.Changes.HasData(); // if we have changes or if we are in re init mode if (hasChanges || context.SyncType != SyncType.Normal) { // 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.InternalDisableConstraintsAsync(context, this.GetSyncAdapter(table, message.Setup), 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) // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { foreach (var table in message.Schema.Tables) { await this.InternalResetTableAsync(context, this.GetSyncAdapter(table, message.Setup), connection, transaction).ConfigureAwait(false); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!message.IsNew && hasChanges) { foreach (var table in message.Schema.Tables.Reverse()) { await this.InternalApplyTableChangesAsync(context, table, message, connection, transaction, DataRowState.Deleted, changesApplied, cancellationToken, progress).ConfigureAwait(false); } } // ----------------------------------------------------- // 2) Applying Inserts and Updates. Apply in table order // ----------------------------------------------------- if (hasChanges) { foreach (var table in message.Schema.Tables) { await this.InternalApplyTableChangesAsync(context, table, 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.InternalEnableConstraintsAsync(context, this.GetSyncAdapter(table, message.Setup), connection, transaction).ConfigureAwait(false); } } } // Before cleaning, check if we are not applying changes from a snapshotdirectory var cleanFolder = message.CleanFolder; if (cleanFolder) { cleanFolder = await this.InternalCanCleanFolderAsync(context, message.Changes, cancellationToken, progress).ConfigureAwait(false); } // clear the changes because we don't need them anymore message.Changes.Clear(cleanFolder); var databaseChangesAppliedArgs = new DatabaseChangesAppliedArgs(context, changesApplied, connection, transaction); await this.InterceptAsync(databaseChangesAppliedArgs, cancellationToken).ConfigureAwait(false); this.ReportProgress(context, progress, databaseChangesAppliedArgs); return(context, changesApplied); }
private async Task <(int rowsAppliedCount, int conflictsResolvedCount, int syncErrorsCount)> ResolveConflictsAsync(SyncContext context, Guid localScopeId, Guid senderScopeId, DbSyncAdapter syncAdapter, List <SyncConflict> conflicts, MessageApplyChanges message, DbConnection connection, DbTransaction transaction) { int rowsAppliedCount = 0; int conflictsResolvedCount = 0; int syncErrorsCount = 0; // If conflicts occured // Eventuall, conflicts are resolved on server side. if (conflicts == null || conflicts.Count <= 0) { return(rowsAppliedCount, conflictsResolvedCount, syncErrorsCount); } foreach (var conflict in conflicts) { var fromScopeLocalTimeStamp = message.LastTimestamp; this.Orchestrator.logger.LogDebug(SyncEventsId.ResolveConflicts, conflict); this.Orchestrator.logger.LogDebug(SyncEventsId.ResolveConflicts, new { LocalScopeId = localScopeId, SenderScopeId = senderScopeId, FromScopeLocalTimeStamp = fromScopeLocalTimeStamp, message.Policy });; var(conflictResolvedCount, resolvedRow, rowAppliedCount) = await this.HandleConflictAsync(localScopeId, senderScopeId, syncAdapter, context, conflict, message.Policy, fromScopeLocalTimeStamp, connection, transaction).ConfigureAwait(false); if (resolvedRow != null) { conflictsResolvedCount += conflictResolvedCount; rowsAppliedCount += rowAppliedCount; } } return(rowsAppliedCount, conflictsResolvedCount, syncErrorsCount); }
InternalApplyChangesAsync(ClientScopeInfo clientScopeInfo, SyncContext context, BatchInfo serverBatchInfo, long clientTimestamp, long remoteClientTimestamp, ConflictResolutionPolicy policy, bool snapshotApplied, DatabaseChangesSelected allChangesSelected, DbConnection connection = default, DbTransaction transaction = default, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null) { try { // lastSyncTS : apply lines only if they are not modified since last client sync var lastTimestamp = clientScopeInfo.LastSyncTimestamp; // isNew : if IsNew, don't apply deleted rows from server var isNew = clientScopeInfo.IsNewScope; // We are in downloading mode // Create the message containing everything needed to apply changes var applyChanges = new MessageApplyChanges(clientScopeInfo.Id, Guid.Empty, isNew, lastTimestamp, clientScopeInfo.Schema, policy, this.Options.DisableConstraintsOnApplyChanges, this.Options.CleanMetadatas, this.Options.CleanFolder, snapshotApplied, serverBatchInfo); DatabaseChangesApplied clientChangesApplied; await using var runner = await this.GetConnectionAsync(context, SyncMode.Writing, SyncStage.ChangesApplying, connection, transaction, cancellationToken, progress).ConfigureAwait(false); context.SyncWay = SyncWay.Download; // Call apply changes on provider (context, clientChangesApplied) = await this.InternalApplyChangesAsync(clientScopeInfo, context, applyChanges, runner.Connection, runner.Transaction, cancellationToken, progress).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // check if we need to delete metadatas if (this.Options.CleanMetadatas && clientChangesApplied.TotalAppliedChanges > 0 && lastTimestamp.HasValue) { List <ClientScopeInfo> allScopes; (context, allScopes) = await this.InternalLoadAllClientScopesInfoAsync(context, runner.Connection, runner.Transaction, runner.CancellationToken, runner.Progress).ConfigureAwait(false); if (allScopes.Count > 0) { // Get the min value from LastSyncTimestamp from all scopes var minLastTimeStamp = allScopes.Min(scope => scope.LastSyncTimestamp.HasValue ? scope.LastSyncTimestamp.Value : Int64.MaxValue); minLastTimeStamp = minLastTimeStamp > lastTimestamp.Value ? lastTimestamp.Value : minLastTimeStamp; (context, _) = await this.InternalDeleteMetadatasAsync(allScopes, context, minLastTimeStamp, runner.Connection, runner.Transaction, cancellationToken, progress).ConfigureAwait(false); } } // now the sync is complete, remember the time this.CompleteTime = DateTime.UtcNow; // generate the new scope item clientScopeInfo.IsNewScope = false; clientScopeInfo.LastSync = this.CompleteTime; clientScopeInfo.LastSyncTimestamp = clientTimestamp; clientScopeInfo.LastServerSyncTimestamp = remoteClientTimestamp; clientScopeInfo.LastSyncDuration = this.CompleteTime.Value.Subtract(context.StartTime).Ticks; // Write scopes locally (context, clientScopeInfo) = await this.InternalSaveClientScopeInfoAsync(clientScopeInfo, context, runner.Connection, runner.Transaction, cancellationToken, progress).ConfigureAwait(false); await runner.CommitAsync().ConfigureAwait(false); return(context, clientChangesApplied, clientScopeInfo); } catch (Exception ex) { throw GetSyncError(context, ex); } }