/// <summary> /// Provision the orchestrator database based on the schema argument, and the provision enumeration /// </summary> /// <param name="schema">Schema to be applied to the database managed by the orchestrator, through the provider.</param> /// <param name="provision">Provision enumeration to determine which components to apply</param> /// <returns>Full schema with table and columns properties</returns> public virtual async Task <SyncSet> ProvisionAsync(SyncSet schema, SyncProvision provision, 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(); using (var connection = this.Provider.CreateConnection()) { // log this.logger.LogInformation(SyncEventsId.Provision, new { connection.Database, Provision = provision }); try { ctx.SyncStage = SyncStage.Provisioning; // If schema does not have any table, just return if (schema == null || schema.Tables == null || !schema.HasTables) { throw new MissingTablesException(); } if (this is LocalOrchestrator && (provision.HasFlag(SyncProvision.ServerHistoryScope) || provision.HasFlag(SyncProvision.ServerScope))) { throw new InvalidProvisionForLocalOrchestratorException(); } else if (!(this is LocalOrchestrator) && provision.HasFlag(SyncProvision.ClientScope)) { throw new InvalidProvisionForRemoteOrchestratorException(); } // 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); // Check if we have tables AND columns // If we don't have any columns it's most probably because user called method with the Setup only // So far we have only tables names, it's enough to get the schema if (schema.HasTables && !schema.HasColumns) { this.logger.LogInformation(SyncEventsId.GetSchema, this.Setup); (ctx, schema) = await this.Provider.GetSchemaAsync(ctx, this.Setup, connection, transaction, cancellationToken, progress).ConfigureAwait(false); } if (!schema.HasTables) { throw new MissingTablesException(); } if (!schema.HasColumns) { throw new MissingColumnsException(); } await this.InterceptAsync(new DatabaseProvisioningArgs(ctx, provision, schema, connection, transaction), cancellationToken).ConfigureAwait(false); await this.Provider.ProvisionAsync(ctx, schema, this.Setup, provision, this.Options.ScopeInfoTableName, connection, transaction, cancellationToken, progress).ConfigureAwait(false); await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false); transaction.Commit(); } ctx.SyncStage = SyncStage.Provisioned; await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false); var args = new DatabaseProvisionedArgs(ctx, provision, schema, connection); await this.InterceptAsync(args, cancellationToken).ConfigureAwait(false); this.ReportProgress(ctx, progress, args); } catch (Exception ex) { RaiseError(ex); } finally { if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } } return(schema); } }
/// <summary> /// Migrate an old setup configuration to a new one. This method is usefull if you are changing your SyncSetup when a database has been already configured previously /// </summary> public virtual async Task MigrationAsync(SyncSetup oldSetup, SyncSet schema, 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(); using (var connection = this.Provider.CreateConnection()) { try { ctx.SyncStage = SyncStage.Migrating; // If schema does not have any table, just return if (schema == null || schema.Tables == null || !schema.HasTables) { throw new MissingTablesException(); } // Open connection await this.OpenConnectionAsync(connection, cancellationToken).ConfigureAwait(false); SyncProvision provision = SyncProvision.None; // Create a transaction using (var transaction = connection.BeginTransaction()) { await this.InterceptAsync(new TransactionOpenedArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false); // Launch InterceptAsync on Migrating await this.InterceptAsync(new DatabaseMigratingArgs(ctx, schema, oldSetup, this.Setup, connection, transaction), cancellationToken).ConfigureAwait(false); this.logger.LogDebug(SyncEventsId.Migration, oldSetup); this.logger.LogDebug(SyncEventsId.Migration, this.Setup); // Migrate the db structure await this.Provider.MigrationAsync(ctx, schema, oldSetup, this.Setup, true, connection, transaction, cancellationToken, progress); // Now call the ProvisionAsync() to provision new tables provision = SyncProvision.Table | SyncProvision.TrackingTable | SyncProvision.StoredProcedures | SyncProvision.Triggers; await this.InterceptAsync(new DatabaseProvisioningArgs(ctx, provision, schema, connection, transaction), cancellationToken).ConfigureAwait(false); // Provision new tables if needed await this.Provider.ProvisionAsync(ctx, schema, this.Setup, provision, this.Options.ScopeInfoTableName, connection, transaction, cancellationToken, progress).ConfigureAwait(false); ScopeInfo localScope = null; ctx = await this.Provider.EnsureClientScopeAsync(ctx, this.Options.ScopeInfoTableName, connection, transaction, cancellationToken, progress).ConfigureAwait(false); (ctx, localScope) = await this.Provider.GetClientScopeAsync(ctx, this.Options.ScopeInfoTableName, this.ScopeName, connection, transaction, cancellationToken, progress).ConfigureAwait(false); localScope.Setup = this.Setup; localScope.Schema = schema; // Write scopes locally ctx = await this.Provider.WriteClientScopeAsync(ctx, this.Options.ScopeInfoTableName, localScope, connection, transaction, cancellationToken, progress).ConfigureAwait(false); await this.InterceptAsync(new TransactionCommitArgs(ctx, connection, transaction), cancellationToken).ConfigureAwait(false); transaction.Commit(); } ctx.SyncStage = SyncStage.Migrated; await this.CloseConnectionAsync(connection, cancellationToken).ConfigureAwait(false); var args = new DatabaseProvisionedArgs(ctx, provision, schema, connection); await this.InterceptAsync(args, cancellationToken).ConfigureAwait(false); this.ReportProgress(ctx, progress, args); // InterceptAsync Migrated var args2 = new DatabaseMigratedArgs(ctx, schema, this.Setup); await this.InterceptAsync(args2, cancellationToken).ConfigureAwait(false); this.ReportProgress(ctx, progress, args2); } catch (Exception ex) { RaiseError(ex); } finally { if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } } } }
/// <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, MessageEnsureDatabase message) { DbConnection connection = null; try { // Event progress context.SyncStage = SyncStage.DatabaseApplying; var script = new StringBuilder(); // Open the connection using (connection = this.CreateConnection()) { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { // Interceptor var beforeArgs = new DatabaseProvisioningArgs(context, SyncProvision.All, message.Schema, connection, transaction); await this.InterceptAsync(beforeArgs); if (message.ScopeInfo.LastSync.HasValue && !beforeArgs.OverwriteSchema) { return(context); } // Sorting tables based on dependencies between them var dmTables = message.Schema.Tables .SortByDependencies(tab => tab.ChildRelations .Select(r => r.ChildTable)); foreach (var dmTable in dmTables) { var builder = this.GetDatabaseBuilder(dmTable); // set if the builder supports creating the bulk operations proc stock builder.UseBulkProcedures = this.SupportBulkOperations; // adding filter this.AddFilters(message.Filters, dmTable, builder); context.SyncStage = SyncStage.DatabaseTableApplying; // Launch any interceptor if available await this.InterceptAsync(new TableProvisioningArgs(context, SyncProvision.All, dmTable, connection, transaction)); 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); // Report & Interceptor context.SyncStage = SyncStage.DatabaseTableApplied; var tableProvisionedArgs = new TableProvisionedArgs(context, SyncProvision.All, dmTable, connection, transaction); this.ReportProgress(context, tableProvisionedArgs); await this.InterceptAsync(tableProvisionedArgs); } // Report & Interceptor context.SyncStage = SyncStage.DatabaseApplied; var args = new DatabaseProvisionedArgs(context, SyncProvision.All, message.Schema, script.ToString(), connection, transaction); this.ReportProgress(context, args); await this.InterceptAsync(args); transaction.Commit(); } connection.Close(); return(context); } } catch (Exception ex) { throw new SyncException(ex, SyncStage.DatabaseApplying); } finally { if (connection != null && connection.State != ConnectionState.Closed) { connection.Close(); } } }
/// <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, SyncSet schema, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress = null) { // Event progress context.SyncStage = SyncStage.SchemaApplying; var script = new StringBuilder(); var beforeArgs = new DatabaseProvisioningArgs(context, SyncProvision.All, schema, connection, transaction); await this.InterceptAsync(beforeArgs).ConfigureAwait(false); // get Database builder var builder = this.GetDatabaseBuilder(); builder.UseChangeTracking = this.UseChangeTracking; builder.UseBulkProcedures = this.SupportBulkOperations; // Initialize database if needed builder.EnsureDatabase(connection, transaction); // Sorting tables based on dependencies between them var schemaTables = schema.Tables .SortByDependencies(tab => tab.GetRelations() .Select(r => r.GetParentTable())); foreach (var schemaTable in schemaTables) { var tableBuilder = this.GetTableBuilder(schemaTable); // set if the builder supports creating the bulk operations proc stock tableBuilder.UseBulkProcedures = this.SupportBulkOperations; tableBuilder.UseChangeTracking = this.UseChangeTracking; // adding filter this.AddFilters(schemaTable, tableBuilder); context.SyncStage = SyncStage.TableSchemaApplying; // Launch any interceptor if available await this.InterceptAsync(new TableProvisioningArgs(context, SyncProvision.All, schemaTable, connection, transaction)).ConfigureAwait(false); tableBuilder.Create(connection, transaction); tableBuilder.CreateForeignKeys(connection, transaction); // Report & Interceptor context.SyncStage = SyncStage.TableSchemaApplied; var tableProvisionedArgs = new TableProvisionedArgs(context, SyncProvision.All, schemaTable, connection, transaction); this.ReportProgress(context, progress, tableProvisionedArgs); await this.InterceptAsync(tableProvisionedArgs).ConfigureAwait(false); } // Report & Interceptor context.SyncStage = SyncStage.SchemaApplied; var args = new DatabaseProvisionedArgs(context, SyncProvision.All, schema, script.ToString(), connection, transaction); this.ReportProgress(context, progress, args); await this.InterceptAsync(args).ConfigureAwait(false); await this.InterceptAsync(new TransactionCommitArgs(context, connection, transaction)).ConfigureAwait(false); return(context); }