/// <summary> /// Write scope in the provider datasource /// </summary> public virtual async Task <SyncContext> WriteScopesAsync(SyncContext context, List <ScopeInfo> scopes) { // Open the connection using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { var scopeBuilder = this.GetScopeBuilder(); var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(connection, transaction); var lstScopes = new List <ScopeInfo>(); foreach (var scope in scopes) { lstScopes.Add(scopeInfoBuilder.InsertOrUpdateScopeInfo(scope)); } context.SyncStage = SyncStage.ScopeSaved; // Event progress this.TryRaiseProgressEvent( new ScopeEventArgs(this.ProviderTypeName, context.SyncStage, lstScopes.FirstOrDefault(s => s.IsLocal)), ScopeSaved); transaction.Commit(); } } catch (DbException dbex) { throw SyncException.CreateDbException(context.SyncStage, dbex); } catch (Exception ex) { Debug.WriteLine(ex.Message); if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } throw; } finally { if (connection.State != ConnectionState.Closed) { connection.Close(); } } return(context); } }
internal static SyncException CreateArgumentException(SyncStage syncStage, string paramName, string message = null) { var m = message ?? $"Argument exception on parameter {paramName}"; SyncException syncException = new SyncException(m, syncStage, SyncExceptionType.Argument); syncException.Argument = paramName; return(syncException); }
/// <summary> /// Encapsulates an error in a SyncException, let provider enrich the error if needed, then throw again /// </summary> internal void RaiseError(Exception exception) { var syncException = new SyncException(exception, this.GetContext().SyncStage); // try to let the provider enrich the exception this.Provider.EnsureSyncException(syncException); syncException.Side = this.Side; this.logger.LogError(SyncEventsId.Exception, syncException, syncException.Message); throw syncException; }
/// <summary> /// Gets a batch of changes to synchronize when given batch size, /// destination knowledge, and change data retriever parameters. /// </summary> /// <returns>A DbSyncContext object that will be used to retrieve the modified data.</returns> public virtual async Task <(SyncContext, BatchInfo, ChangesSelected)> GetChangeBatchAsync(SyncContext context, ScopeInfo scopeInfo) { try { if (scopeInfo == null) { throw new ArgumentException("ClientScope is null"); } var configuration = GetCacheConfiguration(); // check batchSize if not > then Configuration.DownloadBatchSizeInKB if (configuration.DownloadBatchSizeInKB > 0) { Debug.WriteLine($"Enumeration data cache size selected: {configuration.DownloadBatchSizeInKB} Kb"); } // bacth info containing changes BatchInfo batchInfo; // Statistics about changes that are selected ChangesSelected changesSelected; (context, batchInfo, changesSelected) = await this.GetChanges(context, scopeInfo); // Check if the remote is not outdated var isOutdated = this.IsRemoteOutdated(); if (isOutdated) { throw new Exception("OutDatedPeer"); } return(context, batchInfo, changesSelected); } catch (DbException dbex) { throw SyncException.CreateDbException(context.SyncStage, dbex); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } }
/// <summary> /// Try to raise a generalist progress event /// </summary> private void TryRaiseProgressEvent(SyncStage stage, String message, Dictionary <String, String> properties = null) { ProgressEventArgs progressEventArgs = new ProgressEventArgs(this.ProviderTypeName, stage, message); if (properties != null) { progressEventArgs.Properties = properties; } SyncProgress?.Invoke(this, progressEventArgs); if (progressEventArgs.Action == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(progressEventArgs.Stage); } }
private static void HandleSyncError(SyncException sex) { switch (sex.SyncStage) { case SyncStage.BeginSession: Logger.Current.Info(sex.ToString()); break; case SyncStage.EnsureConfiguration: break; case SyncStage.SelectedChanges: break; case SyncStage.ApplyingChanges: break; case SyncStage.AppliedChanges: break; case SyncStage.ApplyingInserts: break; case SyncStage.ApplyingUpdates: break; case SyncStage.ApplyingDeletes: break; case SyncStage.WriteMetadata: break; case SyncStage.EndSession: Logger.Current.Info(sex.ToString()); break; case SyncStage.CleanupMetadata: break; default: break; } // try to end sessions on both }
/// <summary> /// Called when the sync is over /// </summary> public virtual Task <SyncContext> EndSessionAsync(SyncContext context) { try { // already ended lock (this) { if (!syncInProgress) { return(Task.FromResult(context)); } } Debug.WriteLine($"EndSession() called on Provider {this.ProviderTypeName}"); context.SyncStage = SyncStage.EndSession; // Event progress this.TryRaiseProgressEvent( new EndSessionEventArgs(this.ProviderTypeName, context.SyncStage), this.EndSession); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } finally { lock (this) { this.syncInProgress = false; } } return(Task.FromResult(context)); }
/// <summary> /// Read a scope info /// </summary> public virtual async Task <(SyncContext, long)> GetLocalTimestampAsync(SyncContext context) { // Open the connection using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); var scopeBuilder = this.GetScopeBuilder(); var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(connection); var localTime = scopeInfoBuilder.GetLocalTimestamp(); return(context, localTime); } catch (DbException dbex) { throw SyncException.CreateDbException(context.SyncStage, dbex); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } finally { if (connection.State != ConnectionState.Closed) { connection.Close(); } } } }
/// <summary> /// Called by the to indicate that a /// synchronization session has started. /// </summary> public virtual Task <SyncContext> BeginSessionAsync(SyncContext context) { try { Debug.WriteLine($"BeginSession() called on Provider {this.ProviderTypeName}"); lock (this) { if (this.syncInProgress) { throw SyncException.CreateInProgressException(context.SyncStage); } this.syncInProgress = true; } // Set stage context.SyncStage = SyncStage.BeginSession; // Event progress var progressEventArgs = new BeginSessionEventArgs(this.ProviderTypeName, context.SyncStage); this.TryRaiseProgressEvent(progressEventArgs, this.BeginSession); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } return(Task.FromResult(context)); }
internal static Exception CreateInProgressException(SyncStage syncStage) { SyncException syncException = new SyncException("Session already in progress", syncStage, SyncExceptionType.Rollback); return(syncException); }
internal static SyncException CreateUnknowException(SyncStage stage, Exception ex) { SyncException syncException = new SyncException("Unknown error has occured", stage, ex, SyncExceptionType.Unknown); return(syncException); }
/// <summary> /// Create a rollback exception to rollback the Sync session /// </summary> /// <param name="context"></param> internal static SyncException CreateRollbackException(SyncStage stage) { SyncException syncException = new SyncException("User rollback action.", stage, SyncExceptionType.Rollback); return(syncException); }
internal static SyncException CreateDbException(SyncStage syncStage, DbException dbex) { SyncException syncException = new SyncException(dbex.Message, syncStage, dbex, SyncExceptionType.DataStore); return(syncException); }
/// <summary> /// Deprovision a database. You have to passe a configuration object, containing at least the dmTables /// </summary> public async Task DeprovisionAsync(string[] tables, SyncProvision provision) { if (tables == null || tables.Length == 0) { throw new SyncException("You must set the tables you want to modify"); } // Load the configuration var configuration = await this.ReadConfigurationAsync(tables); // Open the connection using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { for (int i = configuration.Count - 1; i >= 0; i--) { // Get the table var dmTable = configuration[i]; // get the builder var builder = GetDatabaseBuilder(dmTable); // adding filters this.AddFilters(configuration, dmTable, builder); if (provision.HasFlag(SyncProvision.TrackingTable) || provision == SyncProvision.All) { builder.DropTrackingTable(connection, transaction); } if (provision.HasFlag(SyncProvision.StoredProcedures) || provision == SyncProvision.All) { builder.DropProcedures(connection, transaction); } if (provision.HasFlag(SyncProvision.Triggers) || provision == SyncProvision.All) { builder.DropTriggers(connection, transaction); } if (provision.HasFlag(SyncProvision.Table) || provision == SyncProvision.All) { builder.DropTable(connection, transaction); } } transaction.Commit(); } } catch (Exception ex) { throw SyncException.CreateUnknowException(SyncStage.BeginSession, ex); } finally { if (connection.State != ConnectionState.Closed) { connection.Close(); } } } }
internal static SyncException CreateNotSupportedException(SyncStage syncStage, string notSupportedMessage) { SyncException syncException = new SyncException(notSupportedMessage, syncStage, SyncExceptionType.NotSupported); return(syncException); }
/// <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, ScopeInfo fromScope, BatchInfo changes) { try { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; ChangesApplied changesApplied = new ChangesApplied(); using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); Debug.WriteLine($"----- Applying Changes for Scope \"{fromScope.Name}\" -----"); Debug.WriteLine(""); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, connection, applyTransaction, fromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!fromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } // ----------------------------------------------------- // 1) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } applyTransaction.Commit(); Debug.WriteLine($"--- End Applying Changes for Scope \"{fromScope.Name}\" ---"); Debug.WriteLine(""); } catch (Exception exception) { Debug.WriteLine($"Caught exception while applying changes: {exception}"); throw; } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (changes != null) { changes.Clear(); } } return(context, changesApplied); } } catch (Exception) { throw; } }
/// <summary> /// Let a chance to provider to enrich SyncExecption /// </summary> public virtual void EnsureSyncException(SyncException syncException) { }
/// <summary> /// Called when the sync ensure scopes are created /// </summary> public virtual async Task <(SyncContext, List <ScopeInfo>)> EnsureScopesAsync(SyncContext context, string scopeName, Guid?clientReferenceId = null) { try { if (string.IsNullOrEmpty(scopeName)) { throw SyncException.CreateArgumentException(SyncStage.ScopeSaved, "ScopeName"); } context.SyncStage = SyncStage.ScopeLoading; List <ScopeInfo> scopes = new List <ScopeInfo>(); // Open the connection using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { var scopeBuilder = this.GetScopeBuilder(); var scopeInfoBuilder = scopeBuilder.CreateScopeInfoBuilder(connection, transaction); var needToCreateScopeInfoTable = scopeInfoBuilder.NeedToCreateScopeInfoTable(); // create the scope info table if needed if (needToCreateScopeInfoTable) { scopeInfoBuilder.CreateScopeInfoTable(); } // not the first time we ensure scopes, so get scopes if (!needToCreateScopeInfoTable) { // get all scopes shared by all (identified by scopeName) var lstScopes = scopeInfoBuilder.GetAllScopes(scopeName); // try to get the scopes from database // could be two scopes if from server or a single scope if from client scopes = lstScopes.Where(s => (s.IsLocal == true || (clientReferenceId.HasValue && s.Id == clientReferenceId.Value))).ToList(); } // If no scope found, create it on the local provider if (scopes == null || scopes.Count <= 0) { scopes = new List <ScopeInfo>(); // create a new scope id for the current owner (could be server or client as well) var scope = new ScopeInfo(); scope.Id = Guid.NewGuid(); scope.Name = scopeName; scope.IsLocal = true; scope.IsNewScope = true; scope.LastSync = null; scope = scopeInfoBuilder.InsertOrUpdateScopeInfo(scope); scopes.Add(scope); } else { //check if we have alread a good last sync. if no, treat it as new scopes.ForEach(sc => sc.IsNewScope = sc.LastSync == null); } // if we are not on the server, we have to check that we only have one scope if (!clientReferenceId.HasValue && scopes.Count > 1) { throw SyncException.CreateNotSupportedException(SyncStage.ScopeSaved, "On Local provider, we should have only one scope info"); } // if we have a reference in args, we need to get this specific line from database // this happen only on the server side if (clientReferenceId.HasValue) { var refScope = scopes.FirstOrDefault(s => s.Id == clientReferenceId); if (refScope == null) { refScope = new ScopeInfo(); refScope.Id = clientReferenceId.Value; refScope.Name = scopeName; refScope.IsLocal = false; refScope.IsNewScope = true; refScope.LastSync = null; refScope = scopeInfoBuilder.InsertOrUpdateScopeInfo(refScope); scopes.Add(refScope); } else { refScope.IsNewScope = refScope.LastSync == null; } } transaction.Commit(); } } catch (DbException dbex) { throw SyncException.CreateDbException(context.SyncStage, dbex); } catch (Exception dbex) { throw SyncException.CreateUnknowException(context.SyncStage, dbex); } finally { if (connection.State != ConnectionState.Closed) { connection.Close(); } } } // Event progress this.TryRaiseProgressEvent( new ScopeEventArgs(this.ProviderTypeName, context.SyncStage, scopes.FirstOrDefault(s => s.IsLocal)), ScopeLoading); return(context, scopes); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } }
/// <summary> /// Generate the DmTable configuration from a given columns list /// Validate that all columns are currently supported by the provider /// </summary> private void ValidateTableFromColumnsList(DmTable dmTable, List <DmColumn> columns, IDbManagerTable dbManagerTable) { dmTable.OriginalProvider = this.ProviderTypeName; var ordinal = 0; if (columns == null || columns.Count <= 0) { throw new SyncException($"{dmTable.TableName} does not contains any columns.", SyncStage.ConfigurationApplying, SyncExceptionType.NotSupported); } // Get PrimaryKey var dmTableKeys = dbManagerTable.GetTablePrimaryKeys(); if (dmTableKeys == null || dmTableKeys.Count == 0) { throw new SyncException($"No Primary Keys in table {dmTable.TableName}, Can't make a synchronization with a table without primary keys.", SyncStage.ConfigurationApplying, SyncExceptionType.NoPrimaryKeys); } // Check if we have more than one column (excepting primarykeys) var columnsNotPkeys = columns.Count(c => !dmTableKeys.Contains(c.ColumnName)); if (columnsNotPkeys <= 0) { throw new SyncException($"{dmTable.TableName} does not contains any columns, excepting primary keys.", SyncStage.ConfigurationApplying, SyncExceptionType.NotSupported); } foreach (var column in columns.OrderBy(c => c.Ordinal)) { // First of all validate if the column is currently supported if (!Metadata.IsValid(column)) { throw SyncException.CreateNotSupportedException( SyncStage.ConfigurationApplying, $"The Column {column.ColumnName} of type {column.OriginalTypeName} from provider {this.ProviderTypeName} is not currently supported."); } var columnNameLower = column.ColumnName.ToLowerInvariant(); if (columnNameLower == "sync_scope_name" || columnNameLower == "scope_timestamp" || columnNameLower == "scope_is_local" || columnNameLower == "scope_last_sync" || columnNameLower == "create_scope_id" || columnNameLower == "update_scope_id" || columnNameLower == "create_timestamp" || columnNameLower == "update_timestamp" || columnNameLower == "timestamp" || columnNameLower == "sync_row_is_tombstone" || columnNameLower == "last_change_datetime" || columnNameLower == "sync_scope_name" || columnNameLower == "sync_scope_name" ) { throw SyncException.CreateNotSupportedException( SyncStage.ConfigurationApplying, $"The Column name {column.ColumnName} from provider {this.ProviderTypeName} is a reserved column name. Please choose another column name."); } dmTable.Columns.Add(column); // Gets the datastore owner dbType (could be SqlDbtype, MySqlDbType, SqliteDbType, NpgsqlDbType & so on ...) object datastoreDbType = Metadata.ValidateOwnerDbType(column.OriginalTypeName, column.IsUnsigned, column.IsUnicode); // once we have the datastore type, we can have the managed type Type columnType = Metadata.ValidateType(datastoreDbType); // and the DbType column.DbType = Metadata.ValidateDbType(column.OriginalTypeName, column.IsUnsigned, column.IsUnicode); // Gets the owner dbtype (SqlDbType, OracleDbType, MySqlDbType, NpsqlDbType & so on ...) // Sqlite does not have it's own type, so it's DbType too column.OriginalDbType = datastoreDbType.ToString(); // Validate max length column.MaxLength = Metadata.ValidateMaxLength(column.OriginalTypeName, column.IsUnsigned, column.IsUnicode, column.MaxLength); // Validate if column should be readonly column.ReadOnly = Metadata.ValidateIsReadonly(column); // set position ordinal column.SetOrdinal(ordinal); ordinal++; // Validate the precision and scale properties if (Metadata.IsNumericType(column.OriginalTypeName)) { if (Metadata.SupportScale(column.OriginalTypeName)) { var(p, s) = Metadata.ValidatePrecisionAndScale(column); column.Precision = p; column.PrecisionSpecified = true; column.Scale = s; column.ScaleSpecified = true; } else { column.Precision = Metadata.ValidatePrecision(column); column.PrecisionSpecified = true; column.ScaleSpecified = false; } } } DmColumn[] columnsForKey = new DmColumn[dmTableKeys.Count]; for (int i = 0; i < dmTableKeys.Count; i++) { var rowColumn = dmTableKeys[i]; var columnKey = dmTable.Columns.FirstOrDefault(c => String.Equals(c.ColumnName, rowColumn, StringComparison.InvariantCultureIgnoreCase)); columnsForKey[i] = columnKey ?? throw new SyncException("Primary key found is not present in the columns list", SyncStage.ConfigurationApplying, SyncExceptionType.NoPrimaryKeys); } // Set the primary Key dmTable.PrimaryKey = new DmKey(columnsForKey); }
internal static SyncException CreateOperationCanceledException(SyncStage syncStage, OperationCanceledException oce) { SyncException syncException = new SyncException("Operation canceled.", syncStage, SyncExceptionType.OperationCanceled); return(syncException); }
/// <summary> /// Ensure configuration is correct on both server and client side /// </summary> public virtual async Task <(SyncContext, SyncConfiguration)> EnsureConfigurationAsync(SyncContext context, SyncConfiguration configuration = null) { try { context.SyncStage = SyncStage.ConfigurationApplying; // Get cache manager and try to get configuration from cache var cacheManager = this.CacheManager; var cacheConfiguration = GetCacheConfiguration(); // if we don't pass config object (configuration == null), we may be in proxy mode, so the config object is handled by a local configuration object. if (configuration == null && this.syncConfiguration == null) { throw SyncException.CreateArgumentException(SyncStage.ConfigurationApplied, "Configuration", "You try to set a provider with no configuration object"); } // the configuration has been set from the proxy server itself, use it. if (configuration == null && this.syncConfiguration != null) { configuration = this.syncConfiguration; } // Raise event before context.SyncStage = SyncStage.ConfigurationApplying; var beforeArgs2 = new ConfigurationApplyingEventArgs(this.ProviderTypeName, context.SyncStage); this.TryRaiseProgressEvent(beforeArgs2, this.ConfigurationApplying); bool overWriteConfiguration = beforeArgs2.OverwriteConfiguration; // if we have already a cache configuration, we can return, except if we should overwrite it if (cacheConfiguration != null && !overWriteConfiguration) { // Raise event after context.SyncStage = SyncStage.ConfigurationApplied; var afterArgs2 = new ConfigurationAppliedEventArgs(this.ProviderTypeName, context.SyncStage, cacheConfiguration); this.TryRaiseProgressEvent(afterArgs2, this.ConfigurationApplied); // if config has been changed by user, save it again this.SetCacheConfiguration(cacheConfiguration); return(context, cacheConfiguration); } // create local directory if (!String.IsNullOrEmpty(configuration.BatchDirectory) && !Directory.Exists(configuration.BatchDirectory)) { Directory.CreateDirectory(configuration.BatchDirectory); } // if we dont have already read the tables || we want to overwrite the current config if ((configuration.HasTables && !configuration.HasColumns)) { string[] tables = new string[configuration.Count]; for (int i = 0; i < configuration.Count; i++) { // just check if we have a schema var dmTable = configuration[i]; var tableName = String.IsNullOrEmpty(dmTable.Schema) ? dmTable.TableName : $"[{dmTable.Schema}].[{dmTable.TableName}]"; tables[i] = tableName; } await this.ReadConfigurationAsync(configuration); } // save to cache this.SetCacheConfiguration(configuration); context.SyncStage = SyncStage.ConfigurationApplied; var afterArgs = new ConfigurationAppliedEventArgs(this.ProviderTypeName, context.SyncStage, configuration); this.TryRaiseProgressEvent(afterArgs, this.ConfigurationApplied); // if config has been changed by user, save it again this.SetCacheConfiguration(configuration); return(context, configuration); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } }
/// <summary> /// Be sure all tables are ready and configured for sync /// the ScopeSet Configuration MUST be filled by the schema form Database /// </summary> public virtual async Task <SyncContext> EnsureDatabaseAsync(SyncContext context, ScopeInfo scopeInfo) { var configuration = GetCacheConfiguration(); // Event progress context.SyncStage = SyncStage.DatabaseApplying; DatabaseApplyingEventArgs beforeArgs = new DatabaseApplyingEventArgs(this.ProviderTypeName, context.SyncStage, configuration); this.TryRaiseProgressEvent(beforeArgs, this.DatabaseApplying); // if config has been editer by user in event, save again this.SetCacheConfiguration(configuration); // If scope exists and lastdatetime sync is present, so database exists // Check if we don't have an OverwriteConfiguration (if true, we force the check) if (scopeInfo.LastSync.HasValue && !beforeArgs.OverwriteConfiguration) { return(context); } StringBuilder script = new StringBuilder(); // Open the connection using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { foreach (var dmTable in configuration) { var builder = GetDatabaseBuilder(dmTable); // adding filter this.AddFilters(configuration, dmTable, builder); context.SyncStage = SyncStage.DatabaseTableApplying; DatabaseTableApplyingEventArgs beforeTableArgs = new DatabaseTableApplyingEventArgs(this.ProviderTypeName, context.SyncStage, dmTable.TableName); this.TryRaiseProgressEvent(beforeTableArgs, this.DatabaseTableApplying); string currentScript = null; if (beforeArgs.GenerateScript) { currentScript = builder.ScriptTable(connection, transaction); currentScript += builder.ScriptForeignKeys(connection, transaction); script.Append(currentScript); } builder.Create(connection, transaction); builder.CreateForeignKeys(connection, transaction); context.SyncStage = SyncStage.DatabaseTableApplied; DatabaseTableAppliedEventArgs afterTableArgs = new DatabaseTableAppliedEventArgs(this.ProviderTypeName, context.SyncStage, dmTable.TableName, currentScript); this.TryRaiseProgressEvent(afterTableArgs, this.DatabaseTableApplied); } context.SyncStage = SyncStage.DatabaseApplied; var afterArgs = new DatabaseAppliedEventArgs(this.ProviderTypeName, context.SyncStage, script.ToString()); this.TryRaiseProgressEvent(afterArgs, this.DatabaseApplied); transaction.Commit(); } } catch (DbException dbex) { throw SyncException.CreateDbException(context.SyncStage, dbex); } catch (Exception ex) { if (ex is SyncException) { throw; } else { throw SyncException.CreateUnknowException(context.SyncStage, ex); } } finally { if (connection.State != ConnectionState.Closed) { connection.Close(); } } return(context); } }
/// <summary> /// Launch a synchronization with the specified mode /// </summary> public async Task <SyncContext> SynchronizeAsync(SyncType syncType, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(this.scopeName)) { throw new Exception("Scope Name is mandatory"); } // Context, used to back and forth data between servers SyncContext context = new SyncContext(Guid.NewGuid()); // set start time context.StartTime = DateTime.Now; // if any parameters, set in context context.Parameters = this.Parameters; context.SyncType = syncType; this.SessionState = SyncSessionState.Synchronizing; this.SessionStateChanged?.Invoke(this, this.SessionState); ScopeInfo localScopeInfo = null, serverScopeInfo = null, localScopeReferenceInfo = null, scope = null; Guid fromId = Guid.Empty; long lastSyncTS = 0L; bool isNew = true; // Stats computed ChangesStatistics changesStatistics = new ChangesStatistics(); // tmp check error bool hasErrors = false; try { if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Setting the cancellation token this.LocalProvider.SetCancellationToken(cancellationToken); this.RemoteProvider.SetCancellationToken(cancellationToken); // Begin Session / Read the adapters context = await this.RemoteProvider.BeginSessionAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context = await this.LocalProvider.BeginSessionAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 1) Read scope info // ---------------------------------------- // get the scope from local provider List <ScopeInfo> localScopes; List <ScopeInfo> serverScopes; (context, localScopes) = await this.LocalProvider.EnsureScopesAsync(context, scopeName); if (localScopes.Count != 1) { throw new Exception("On Local provider, we should have only one scope info"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } localScopeInfo = localScopes[0]; (context, serverScopes) = await this.RemoteProvider.EnsureScopesAsync(context, scopeName, localScopeInfo.Id); if (serverScopes.Count != 2) { throw new Exception("On Remote provider, we should have two scopes (one for server and one for client side)"); } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } serverScopeInfo = serverScopes.First(s => s.Id != localScopeInfo.Id); localScopeReferenceInfo = serverScopes.First(s => s.Id == localScopeInfo.Id); // ---------------------------------------- // 2) Build Configuration Object // ---------------------------------------- // Get Configuration from remote provider (context, this.Configuration) = await this.RemoteProvider.EnsureConfigurationAsync(context, this.Configuration); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Invert policy on the client var configurationLocale = this.Configuration.Clone(); var policy = this.Configuration.ConflictResolutionPolicy; if (policy == ConflictResolutionPolicy.ServerWins) { configurationLocale.ConflictResolutionPolicy = ConflictResolutionPolicy.ClientWins; } if (policy == ConflictResolutionPolicy.ClientWins) { configurationLocale.ConflictResolutionPolicy = ConflictResolutionPolicy.ServerWins; } // Apply on local Provider SyncConfiguration configuration; (context, configuration) = await this.LocalProvider.EnsureConfigurationAsync(context, configurationLocale); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 3) Ensure databases are ready // ---------------------------------------- // Server should have already the schema context = await this.RemoteProvider.EnsureDatabaseAsync(context, serverScopeInfo, DbBuilderOption.CreateOrUseExistingSchema | DbBuilderOption.CreateOrUseExistingTrackingTables); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Client could have, or not, the tables context = await this.LocalProvider.EnsureDatabaseAsync(context, localScopeInfo, DbBuilderOption.CreateOrUseExistingSchema | DbBuilderOption.CreateOrUseExistingTrackingTables); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // ---------------------------------------- // 5) Get changes and apply them // ---------------------------------------- BatchInfo clientBatchInfo; BatchInfo serverBatchInfo; ChangesStatistics clientStatistics = null; ChangesStatistics serverStatistics = null; ChangesStatistics tmpClientStatistics = null; ChangesStatistics tmpServerStatistics = null; // fromId : not really needed on this case, since updated / inserted / deleted row has marked null // otherwise, lines updated by server or others clients are already syncked fromId = localScopeInfo.Id; // lastSyncTS : get lines inserted / updated / deteleted after the last sync commited lastSyncTS = localScopeInfo.LastTimestamp; // isNew : If isNew, lasttimestamp is not correct, so grab all isNew = localScopeInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, clientBatchInfo, clientStatistics) = await this.LocalProvider.GetChangeBatchAsync(context, scope); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Apply on the Server Side // Since we are on the server, // we need to check the server client timestamp (not the client timestamp which is completely different) // fromId : When applying rows, make sure it's identified as applied by this client scope fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastTimestamp; // isNew : not needed isNew = false; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, serverStatistics) = await this.RemoteProvider.ApplyChangesAsync(context, scope, clientBatchInfo); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Get changes from server // fromId : Make sure we don't select lines on server that has been already updated by the client fromId = localScopeInfo.Id; // lastSyncTS : apply lines only if thye are not modified since last client sync lastSyncTS = localScopeReferenceInfo.LastTimestamp; // isNew : make sure we take all lines if it's the first time we get isNew = localScopeReferenceInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, serverBatchInfo, tmpServerStatistics) = await this.RemoteProvider.GetChangeBatchAsync(context, scope); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Update server stats if (serverStatistics == null) { serverStatistics = tmpServerStatistics; } else { clientStatistics.SelectedChanges = tmpServerStatistics.SelectedChanges; } // Apply local changes // fromId : When applying rows, make sure it's identified as applied by this server scope fromId = serverScopeInfo.Id; // lastSyncTS : apply lines only if they are not modified since last client sync lastSyncTS = localScopeInfo.LastTimestamp; // isNew : if IsNew, don't apply deleted rows from server isNew = localScopeInfo.IsNewScope; scope = new ScopeInfo { Id = fromId, IsNewScope = isNew, LastTimestamp = lastSyncTS }; (context, tmpClientStatistics) = await this.LocalProvider.ApplyChangesAsync(context, scope, serverBatchInfo); if (clientStatistics == null) { clientStatistics = tmpClientStatistics; } else { clientStatistics.AppliedChanges = tmpClientStatistics.AppliedChanges; } context.TotalChangesDownloaded = clientStatistics.TotalAppliedChanges; context.TotalChangesUploaded = serverStatistics.TotalAppliedChanges; context.TotalSyncErrors = clientStatistics.TotalAppliedChangesFailed; long serverTimestamp, clientTimestamp; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } (context, serverTimestamp) = await this.RemoteProvider.GetLocalTimestampAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } (context, clientTimestamp) = await this.LocalProvider.GetLocalTimestampAsync(context); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context.CompleteTime = DateTime.Now; serverScopeInfo.IsNewScope = false; localScopeReferenceInfo.IsNewScope = false; localScopeInfo.IsNewScope = false; serverScopeInfo.LastSync = context.CompleteTime; localScopeReferenceInfo.LastSync = context.CompleteTime; localScopeInfo.LastSync = context.CompleteTime; serverScopeInfo.IsLocal = true; localScopeReferenceInfo.IsLocal = false; context = await this.RemoteProvider.WriteScopesAsync(context, new List <ScopeInfo> { serverScopeInfo, localScopeReferenceInfo }); serverScopeInfo.IsLocal = false; localScopeInfo.IsLocal = true; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } context = await this.LocalProvider.WriteScopesAsync(context, new List <ScopeInfo> { localScopeInfo, serverScopeInfo }); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // Begin Session / Read the adapters context = await this.RemoteProvider.EndSessionAsync(context); context = await this.LocalProvider.EndSessionAsync(context); } catch (OperationCanceledException oce) { var error = SyncException.CreateOperationCanceledException(context.SyncStage, oce); HandleSyncError(error); hasErrors = true; throw; } catch (SyncException sex) { HandleSyncError(sex); hasErrors = true; throw; } catch (Exception ex) { var error = SyncException.CreateUnknowException(context.SyncStage, ex); HandleSyncError(error); hasErrors = true; throw; } finally { try { if (hasErrors) { // if EndSessionAsync() was never called, try a last time context = await this.RemoteProvider.EndSessionAsync(context); context = await this.LocalProvider.EndSessionAsync(context); } } catch (Exception) { // no raise } this.SessionState = SyncSessionState.Ready; this.SessionStateChanged?.Invoke(this, this.SessionState); } return(context); }
/// <summary> /// Try to raise a specific progress event /// </summary> private void TryRaiseProgressEvent <T>(T args, EventHandler <T> handler) where T : BaseProgressEventArgs { args.Action = ChangeApplicationAction.Continue; handler?.Invoke(this, args); if (args.Action == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(args.Stage); } var props = new Dictionary <String, String>(); switch (args.Stage) { case SyncStage.None: break; case SyncStage.BeginSession: this.TryRaiseProgressEvent(SyncStage.BeginSession, $"Begin session"); break; case SyncStage.ScopeLoading: props.Add("ScopeId", (args as ScopeEventArgs).ScopeInfo.Id.ToString()); this.TryRaiseProgressEvent(SyncStage.ScopeLoading, $"Loading scope", props); break; case SyncStage.ScopeSaved: props.Add("ScopeId", (args as ScopeEventArgs).ScopeInfo.Id.ToString()); this.TryRaiseProgressEvent(SyncStage.ScopeLoading, $"Scope saved", props); break; case SyncStage.ConfigurationApplying: this.TryRaiseProgressEvent(SyncStage.ConfigurationApplying, $"Applying configuration"); break; case SyncStage.ConfigurationApplied: this.TryRaiseProgressEvent(SyncStage.ConfigurationApplied, $"Configuration applied"); break; case SyncStage.DatabaseApplying: this.TryRaiseProgressEvent(SyncStage.DatabaseApplying, $"Applying database schemas"); break; case SyncStage.DatabaseApplied: props.Add("Script", (args as DatabaseAppliedEventArgs).Script); this.TryRaiseProgressEvent(SyncStage.DatabaseApplied, $"Database schemas applied", props); break; case SyncStage.DatabaseTableApplying: props.Add("TableName", (args as DatabaseTableApplyingEventArgs).TableName); this.TryRaiseProgressEvent(SyncStage.DatabaseApplying, $"Applying schema table", props); break; case SyncStage.DatabaseTableApplied: props.Add("TableName", (args as DatabaseTableAppliedEventArgs).TableName); props.Add("Script", (args as DatabaseTableAppliedEventArgs).Script); this.TryRaiseProgressEvent(SyncStage.DatabaseApplied, $"Table schema applied", props); break; case SyncStage.TableChangesSelecting: props.Add("TableName", (args as TableChangesSelectingEventArgs).TableName); this.TryRaiseProgressEvent(SyncStage.TableChangesSelecting, $"Selecting changes", props); break; case SyncStage.TableChangesSelected: props.Add("TableName", (args as TableChangesSelectedEventArgs).TableChangesSelected.TableName); props.Add("Deletes", (args as TableChangesSelectedEventArgs).TableChangesSelected.Deletes.ToString()); props.Add("Inserts", (args as TableChangesSelectedEventArgs).TableChangesSelected.Inserts.ToString()); props.Add("Updates", (args as TableChangesSelectedEventArgs).TableChangesSelected.Updates.ToString()); props.Add("TotalChanges", (args as TableChangesSelectedEventArgs).TableChangesSelected.TotalChanges.ToString()); this.TryRaiseProgressEvent(SyncStage.TableChangesSelected, $"Changes selected", props); break; case SyncStage.TableChangesApplying: props.Add("TableName", (args as TableChangesApplyingEventArgs).TableName); props.Add("State", (args as TableChangesApplyingEventArgs).State.ToString()); this.TryRaiseProgressEvent(SyncStage.TableChangesApplying, $"Applying changes", props); break; case SyncStage.TableChangesApplied: props.Add("TableName", (args as TableChangesAppliedEventArgs).TableChangesApplied.TableName); props.Add("State", (args as TableChangesAppliedEventArgs).TableChangesApplied.State.ToString()); props.Add("Applied", (args as TableChangesAppliedEventArgs).TableChangesApplied.Applied.ToString()); props.Add("Failed", (args as TableChangesAppliedEventArgs).TableChangesApplied.Failed.ToString()); this.TryRaiseProgressEvent(SyncStage.TableChangesApplied, $"Changes applied", props); break; case SyncStage.EndSession: this.TryRaiseProgressEvent(SyncStage.EndSession, $"End session"); break; case SyncStage.CleanupMetadata: break; } }