// 9 /// <summary> /// Creates a synchronization agent that will handle a full synchronization between a client and a server. /// </summary> /// <param name="clientProvider">local provider to your client database</param> /// <param name="remoteOrchestrator">Remote Orchestrator already configured with a SyncProvider</param> /// <param name="options">Sync Options defining options used by your local provider (and remote provider if type of remoteOrchestrator is not a WebRemoteOrchestrator)</param> public SyncAgent(CoreProvider clientProvider, RemoteOrchestrator remoteOrchestrator, SyncOptions options = default) : this() { if (clientProvider is null) { throw new ArgumentNullException(nameof(clientProvider)); } if (remoteOrchestrator is null) { throw new ArgumentNullException(nameof(remoteOrchestrator)); } if (options == default) { options = new SyncOptions(); } // Override remote orchestrator options, setup and scope name remoteOrchestrator.Options = options; var localOrchestrator = new LocalOrchestrator(clientProvider, options); this.LocalOrchestrator = localOrchestrator; this.RemoteOrchestrator = remoteOrchestrator; this.EnsureOptionsAndSetupInstances(); }
/// <summary> /// Create an agent where the remote orchestrator could be of type RemoteOrchestrator (TCP connection) or WebClientOrchestrator (Http connection) /// </summary> /// <param name="clientProvider">local provider to your client database</param> /// <param name="remoteOrchestrator">remote orchestrator : RemoteOrchestrator or WebClientOrchestrator) </param> /// <param name="setup">Contains list of your tables. Not used if remote orchestrator is WebClientOrchestrator</param> /// <param name="options">Options. Only used on locally if remote orchestrator is WebClientOrchestrator</param> public SyncAgent(CoreProvider clientProvider, IRemoteOrchestrator remoteOrchestrator, SyncSetup setup = null, SyncOptions options = null) : this(new LocalOrchestrator(clientProvider), remoteOrchestrator, setup, options) { }
/// <summary> /// Launch a synchronization with the specified mode /// </summary> public async Task <SyncContext> SynchronizeAsync(SyncType syncType, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) { // for view purpose, if needed if (this.LocalOrchestrator?.Provider != null) { this.LocalOrchestrator.Provider.Options = this.Options; } if (this.RemoteOrchestrator?.Provider != null) { this.RemoteOrchestrator.Provider.Options = this.Options; } // Context, used to back and forth data between servers var context = new SyncContext(Guid.NewGuid()) { // set start time StartTime = DateTime.UtcNow, // if any parameters, set in context Parameters = this.Parameters, // set sync type (Normal, Reinitialize, ReinitializeWithUpload) SyncType = syncType }; this.SessionState = SyncSessionState.Synchronizing; this.SessionStateChanged?.Invoke(this, this.SessionState); try { if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } ScopeInfo scope = null; // Starts sync by : // - Getting local config we have set by code // - Ensure local scope is created (table and values) (context, scope) = await this.LocalOrchestrator.EnsureScopeAsync (context, this.Setup.ScopeName, this.Options.ScopeInfoTableName, cancellationToken, progress); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // check if we already have a schema from local if (!string.IsNullOrEmpty(scope.Schema)) { this.Schema = JsonConvert.DeserializeObject <SyncSet>(scope.Schema); } else { // FIRST call to server // Get the server scope info and server reference id to local scope // Be sure options / schema from client are passed if needed // Then the configuration with full schema var serverSchema = await this.RemoteOrchestrator.EnsureSchemaAsync( context, this.Setup, cancellationToken, remoteProgress); context = serverSchema.context; this.Schema = serverSchema.schema; } if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // on local orchestrator, get local changes // Most probably the schema has changed, so we passed it again (coming from Server) // Don't need to pass again Options since we are not modifying it between server and client var clientChanges = await this.LocalOrchestrator.GetChangesAsync( context, this.Schema, scope, this.Options.BatchSize, this.Options.BatchDirectory, cancellationToken, progress); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // set context context = clientChanges.context; // Get if we need to get all rows from the datasource var fromScratch = scope.IsNewScope || context.SyncType == SyncType.Reinitialize || context.SyncType == SyncType.ReinitializeWithUpload; // IF is new and we have a snapshot directory, try to apply a snapshot if (fromScratch) { // Get snapshot files var serverSnapshotChanges = await this.RemoteOrchestrator.GetSnapshotAsync( context, scope, this.Schema, this.Options.SnapshotsDirectory, this.Options.BatchDirectory, cancellationToken, remoteProgress); if (serverSnapshotChanges.serverBatchInfo != null && serverSnapshotChanges.serverBatchInfo.HasData()) { context = await this.LocalOrchestrator.ApplySnapshotAndGetChangesAsync(context, scope, this.Schema, serverSnapshotChanges.serverBatchInfo, clientChanges.clientTimestamp, serverSnapshotChanges.remoteClientTimestamp, this.Options.DisableConstraintsOnApplyChanges, this.Options.BatchSize, this.Options.BatchDirectory, this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.ScopeInfoTableName, cancellationToken, progress); } } var serverChanges = await this.RemoteOrchestrator.ApplyThenGetChangesAsync( context, scope, this.Schema, clientChanges.clientBatchInfo, this.Options.DisableConstraintsOnApplyChanges, this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.CleanFolder, this.Options.BatchSize, this.Options.BatchDirectory, this.Options.ConflictResolutionPolicy, cancellationToken, remoteProgress); if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } // set context context = serverChanges.context; // Serialize schema to be able to save it in client db if (string.IsNullOrEmpty(scope.Schema)) { var schemaLight = JsonConvert.SerializeObject(this.Schema); scope.Schema = schemaLight; } // Policy is always Server policy, so reverse this policy to get the client policy var clientPolicy = this.Options.ConflictResolutionPolicy == ConflictResolutionPolicy.ServerWins ? ConflictResolutionPolicy.ClientWins : ConflictResolutionPolicy.ServerWins; var localChanges = await this.LocalOrchestrator.ApplyChangesAsync( context, scope, this.Schema, serverChanges.serverBatchInfo, clientPolicy, clientChanges.clientTimestamp, serverChanges.remoteClientTimestamp, this.Options.DisableConstraintsOnApplyChanges, this.Options.UseBulkOperations, this.Options.CleanMetadatas, this.Options.ScopeInfoTableName, true, cancellationToken, progress); context.TotalChangesDownloaded += localChanges.clientChangesApplied.TotalAppliedChanges; context.TotalChangesUploaded += clientChanges.clientChangesSelected.TotalChangesSelected; context.TotalSyncErrors += localChanges.clientChangesApplied.TotalAppliedChangesFailed; if (cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); } } catch (SyncException se) { Debug.WriteLine($"Sync Exception: {se.Message}. TypeName:{se.TypeName}."); throw; } catch (Exception ex) { Debug.WriteLine($"Unknwon Exception: {ex.Message}."); throw new SyncException(ex, SyncStage.None); } finally { // End the current session this.SessionState = SyncSessionState.Ready; this.SessionStateChanged?.Invoke(this, this.SessionState); } return(context); }
/// <summary> /// Create a local orchestrator, used to orchestrates the whole sync on the client side /// </summary> public LocalOrchestrator(CoreProvider provider, SyncOptions options, SyncSetup setup, string scopeName = SyncOptions.DefaultScopeName) : base(provider, options, setup, scopeName) { }