/// <summary> /// Set command parameters value mapped to Row /// </summary> internal void SetColumnParametersValues(DbCommand command, SyncRow row) { if (row.SchemaTable == null) { throw new ArgumentException("Schema table columns does not correspond to row values"); } var schemaTable = row.SchemaTable; foreach (DbParameter parameter in command.Parameters) { if (!string.IsNullOrEmpty(parameter.SourceColumn)) { // foreach parameter, check if we have a column var column = schemaTable.Columns[parameter.SourceColumn]; if (column != null) { object value = row[column] ?? DBNull.Value; DbSyncAdapter.SetParameterValue(command, parameter.ParameterName, value); } } } // return value var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { syncRowCountParam.Direction = ParameterDirection.Output; syncRowCountParam.Value = DBNull.Value; } }
/// <summary> /// Apply a single update in the current datasource. if forceWrite, override conflict situation and force the update /// </summary> private async Task <bool> InternalApplyConflictUpdateAsync(SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, long?lastTimestamp, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { if (row.Table == null) { throw new ArgumentException("Schema table is not present in the row"); } var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateRow, connection, transaction); // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, lastTimestamp, false, forceWrite); var rowUpdatedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowUpdatedCount = (int)syncRowCountParam.Value; } return(rowUpdatedCount > 0); }
/// <summary> /// Update a metadata row /// </summary> internal async Task <(SyncContext context, bool metadataUpdated)> InternalUpdateMetadatasAsync(IScopeInfo scopeInfo, SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { context.SyncStage = SyncStage.ChangesApplying; var(command, _) = await syncAdapter.GetCommandAsync(DbCommandType.UpdateMetadata, connection, transaction); if (command == null) { return(context, false); } // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, 0, row.RowState == DataRowState.Deleted, forceWrite); await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction)).ConfigureAwait(false); var metadataUpdatedRowsCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { metadataUpdatedRowsCount = (int)syncRowCountParam.Value; } command.Dispose(); return(context, metadataUpdatedRowsCount > 0); }
internal virtual async Task <DatabaseMetadatasCleaned> InternalDeleteMetadatasAsync(SyncContext context, SyncSet schema, SyncSetup setup, long timestampLimit, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { await this.InterceptAsync(new MetadataCleaningArgs(context, this.Setup, timestampLimit, connection, transaction), cancellationToken).ConfigureAwait(false); DatabaseMetadatasCleaned databaseMetadatasCleaned = new DatabaseMetadatasCleaned { TimestampLimit = timestampLimit }; foreach (var syncTable in schema.Tables) { // Create sync adapter var syncAdapter = this.GetSyncAdapter(syncTable, setup); var command = await syncAdapter.GetCommandAsync(DbCommandType.DeleteMetadata, connection, transaction); // Set the special parameters for delete metadata DbSyncAdapter.SetParameterValue(command, "sync_row_timestamp", timestampLimit); var rowsCleanedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowsCleanedCount = (int)syncRowCountParam.Value; } // Only add a new table metadata stats object, if we have, at least, purged 1 or more rows if (rowsCleanedCount > 0) { var tableMetadatasCleaned = new TableMetadatasCleaned(syncTable.TableName, syncTable.SchemaName) { RowsCleanedCount = rowsCleanedCount, TimestampLimit = timestampLimit }; databaseMetadatasCleaned.Tables.Add(tableMetadatasCleaned); } } await this.InterceptAsync(new MetadataCleanedArgs(context, databaseMetadatasCleaned, connection), cancellationToken).ConfigureAwait(false); return(databaseMetadatasCleaned); }
/// <summary> /// Internal update untracked rows routine /// </summary> internal async Task <int> InternalUpdateUntrackedRowsAsync(SyncContext ctx, DbSyncAdapter syncAdapter, DbConnection connection, DbTransaction transaction) { // Get correct Select incremental changes command var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateUntrackedRows, connection, transaction); // Execute var rowAffected = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowAffected = (int)syncRowCountParam.Value; } return(rowAffected); }
/// <summary> /// Internal update untracked rows routine /// </summary> internal async Task <(SyncContext context, int updated)> InternalUpdateUntrackedRowsAsync(IScopeInfo scopeInfo, SyncContext context, DbSyncAdapter syncAdapter, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken = default, IProgress <ProgressArgs> progress = null) { // Get table builder var tableBuilder = this.GetTableBuilder(syncAdapter.TableDescription, scopeInfo); // Check if tracking table exists bool trackingTableExists; (context, trackingTableExists) = await this.InternalExistsTrackingTableAsync(scopeInfo, context, tableBuilder, connection, transaction, CancellationToken.None, null).ConfigureAwait(false); if (!trackingTableExists) { throw new MissingTrackingTableException(tableBuilder.TableDescription.GetFullName()); } // Get correct Select incremental changes command var(command, _) = await syncAdapter.GetCommandAsync(DbCommandType.UpdateUntrackedRows, connection, transaction); if (command == null) { return(context, 0); } await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false); // Execute var rowAffected = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowAffected = (int)syncRowCountParam.Value; } command.Dispose(); return(context, rowAffected); }
/// <summary> /// Update a metadata row /// </summary> internal async Task <bool> InternalUpdateMetadatasAsync(SyncContext context, DbSyncAdapter syncAdapter, SyncRow row, Guid?senderScopeId, bool forceWrite, DbConnection connection, DbTransaction transaction) { var command = await syncAdapter.GetCommandAsync(DbCommandType.UpdateMetadata, connection, transaction); // Set the parameters value from row syncAdapter.SetColumnParametersValues(command, row); // Set the special parameters for update syncAdapter.AddScopeParametersValues(command, senderScopeId, 0, row.RowState == DataRowState.Deleted, forceWrite); var metadataUpdatedRowsCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { metadataUpdatedRowsCount = (int)syncRowCountParam.Value; } return(metadataUpdatedRowsCount > 0); }
/// <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); }
/// <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); }
InternalDeleteMetadatasAsync( IEnumerable <IScopeInfo> scopeInfos, SyncContext context, long timestampLimit, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { context.SyncStage = SyncStage.ChangesApplying; var databaseMetadatasCleaned = new DatabaseMetadatasCleaned { TimestampLimit = timestampLimit }; await this.InterceptAsync(new MetadataCleaningArgs(context, scopeInfos, timestampLimit, connection, transaction), progress, cancellationToken).ConfigureAwait(false); // contains all tables already processed var doneList = new List <SetupTable>(); foreach (var scopeInfo in scopeInfos) { if (scopeInfo.Setup?.Tables == null || scopeInfo.Setup.Tables.Count <= 0) { continue; } foreach (var setupTable in scopeInfo.Setup.Tables) { var isDone = doneList.Any(t => t.EqualsByName(setupTable)); if (isDone) { continue; } // create a fake syncTable // Don't need anything else than table name to make a delete metadata clean up var syncTable = new SyncTable(setupTable.TableName, setupTable.SchemaName); // Create sync adapter var syncAdapter = this.GetSyncAdapter(syncTable, scopeInfo); var(command, _) = await syncAdapter.GetCommandAsync(DbCommandType.DeleteMetadata, connection, transaction); if (command != null) { // Set the special parameters for delete metadata DbSyncAdapter.SetParameterValue(command, "sync_row_timestamp", timestampLimit); await this.InterceptAsync(new DbCommandArgs(context, command, connection, transaction), progress, cancellationToken).ConfigureAwait(false); var rowsCleanedCount = await command.ExecuteNonQueryAsync().ConfigureAwait(false); // Check if we have a return value instead var syncRowCountParam = DbSyncAdapter.GetParameter(command, "sync_row_count"); if (syncRowCountParam != null) { rowsCleanedCount = (int)syncRowCountParam.Value; } // Only add a new table metadata stats object, if we have, at least, purged 1 or more rows if (rowsCleanedCount > 0) { var tableMetadatasCleaned = new TableMetadatasCleaned(syncTable.TableName, syncTable.SchemaName) { RowsCleanedCount = rowsCleanedCount, TimestampLimit = timestampLimit }; databaseMetadatasCleaned.Tables.Add(tableMetadatasCleaned); } command.Dispose(); } doneList.Add(setupTable); } } await this.InterceptAsync(new MetadataCleanedArgs(context, databaseMetadatasCleaned, connection), progress, cancellationToken).ConfigureAwait(false); return(context, databaseMetadatasCleaned); }