From cf1d39479008d039b4bdcc3a4091fbdd271e9160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 15 Jun 2023 17:00:56 +0300 Subject: [PATCH 01/14] Created base migration classes --- .../Data/DatabaseMigrationsAvailableEto.cs | 11 + .../DatabaseMigrationEventHandlerBase.cs | 203 ++++++++++++++++++ .../PendingEfCoreMigrationsChecker.cs | 107 +++++++++ 3 files changed, 321 insertions(+) create mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs new file mode 100644 index 0000000000..62eff20b6e --- /dev/null +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.EventBus; + +namespace Volo.Abp.Data; + +[Serializable] +[EventName("abp.data.database_migrations_available")] +public class DatabaseMigrationsAvailableEto +{ + public string DatabaseName { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs new file mode 100644 index 0000000000..3c196d0b6e --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace Volo.Abp.EntityFrameworkCore.Migrations; + +public abstract class DatabaseMigrationEventHandlerBase : ITransientDependency + where TDbContext : DbContext, IEfCoreDbContext +{ + protected const string TryCountPropertyName = "TryCount"; + protected const int MaxEventTryCount = 3; + protected ICurrentTenant CurrentTenant { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected ITenantStore TenantStore { get; } + protected IDistributedEventBus DistributedEventBus { get; } + protected ILogger> Logger { get; } + protected string DatabaseName { get; } + + protected DatabaseMigrationEventHandlerBase( + ILoggerFactory loggerFactory, + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + ITenantStore tenantStore, + IDistributedEventBus distributedEventBus, + string databaseName) + { + CurrentTenant = currentTenant; + UnitOfWorkManager = unitOfWorkManager; + TenantStore = tenantStore; + DatabaseName = databaseName; + DistributedEventBus = distributedEventBus; + + Logger = loggerFactory.CreateLogger>(); + } + + /// + /// Apply pending EF Core schema migrations to the database. + /// Returns true if any migration has applied. + /// + protected virtual async Task MigrateDatabaseSchemaAsync(Guid? tenantId) + { + var result = false; + + using (CurrentTenant.Change(tenantId)) + { + using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) + { + async Task MigrateDatabaseSchemaWithDbContextAsync() + { + var dbContext = await uow.ServiceProvider + .GetRequiredService>() + .GetDbContextAsync(); + + if ((await dbContext.Database.GetPendingMigrationsAsync()).Any()) + { + await dbContext.Database.MigrateAsync(); + return true; + } + + return false; + } + + if (tenantId == null) + { + //Migrating the host database + Logger.LogInformation($"Migrating database of host. Database Name = {DatabaseName}"); + result = await MigrateDatabaseSchemaWithDbContextAsync(); + } + else + { + var tenantConfiguration = await TenantStore.FindAsync(tenantId.Value); + if (!tenantConfiguration.ConnectionStrings.Default.IsNullOrWhiteSpace() || + !tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace()) + { + //Migrating the tenant database (only if tenant has a separate database) + Logger.LogInformation($"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); + result = await MigrateDatabaseSchemaWithDbContextAsync(); + } + } + + await uow.CompleteAsync(); + } + } + + return result; + } + + protected virtual async Task HandleErrorOnApplyDatabaseMigrationAsync( + ApplyDatabaseMigrationsEto eventData, + Exception exception) + { + var tryCount = IncrementEventTryCount(eventData); + if (tryCount <= MaxEventTryCount) + { + Logger.LogWarning($"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); + Logger.LogException(exception, LogLevel.Warning); + + await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await DistributedEventBus.PublishAsync(eventData); + } + else + { + Logger.LogError($"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); + Logger.LogException(exception); + } + } + + protected virtual async Task HandleErrorTenantCreatedAsync( + TenantCreatedEto eventData, + Exception exception) + { + var tryCount = IncrementEventTryCount(eventData); + if (tryCount <= MaxEventTryCount) + { + Logger.LogWarning($"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogException(exception, LogLevel.Warning); + + await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await DistributedEventBus.PublishAsync(eventData); + } + else + { + Logger.LogError($"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogException(exception); + } + } + + protected virtual async Task HandleErrorTenantConnectionStringUpdatedAsync( + TenantConnectionStringUpdatedEto eventData, + Exception exception) + { + var tryCount = IncrementEventTryCount(eventData); + if (tryCount <= MaxEventTryCount) + { + Logger.LogWarning($"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogException(exception, LogLevel.Warning); + + await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await DistributedEventBus.PublishAsync(eventData); + } + else + { + Logger.LogError($"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogException(exception); + } + } + + protected virtual async Task QueueTenantMigrationsAsync() + { + await DistributedEventBus.PublishAsync( + new DatabaseMigrationsAvailableEto + { + DatabaseName = DatabaseName + } + ); + /* + var tenants = await TenantStore.GetListWithSeparateConnectionStringAsync(); + foreach (var tenant in tenants) + { + await DistributedEventBus.PublishAsync( + new ApplyDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = tenant.Id + } + ); + } + */ + } + + private static int GetEventTryCount(EtoBase eventData) + { + var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName); + if (tryCountAsString.IsNullOrEmpty()) + { + return 0; + } + + return int.Parse(tryCountAsString); + } + + private static void SetEventTryCount(EtoBase eventData, int count) + { + eventData.Properties[TryCountPropertyName] = count.ToString(); + } + + private static int IncrementEventTryCount(EtoBase eventData) + { + var count = GetEventTryCount(eventData) + 1; + SetEventTryCount(eventData, count); + return count; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs new file mode 100644 index 0000000000..56680c24e4 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace Volo.Abp.EntityFrameworkCore.Migrations; + +public abstract class PendingEfCoreMigrationsChecker where TDbContext : DbContext, IEfCoreDbContext +{ + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IServiceProvider ServiceProvider { get; } + protected ICurrentTenant CurrentTenant { get; } + protected IDistributedEventBus DistributedEventBus { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected ILogger> Logger { get; } + protected string DatabaseName { get; } + + protected PendingEfCoreMigrationsChecker( + ILoggerFactory loggerFactory, + IUnitOfWorkManager unitOfWorkManager, + IServiceProvider serviceProvider, + ICurrentTenant currentTenant, + IDistributedEventBus distributedEventBus, + IAbpDistributedLock abpDistributedLock, + string databaseName) + { + UnitOfWorkManager = unitOfWorkManager; + ServiceProvider = serviceProvider; + CurrentTenant = currentTenant; + DistributedEventBus = distributedEventBus; + DistributedLock = abpDistributedLock; + DatabaseName = databaseName; + Logger = loggerFactory.CreateLogger>(); + } + + public virtual async Task CheckAndApplyDatabaseMigrationsAsync() + { + await TryAsync(LockAndApplyDatabaseMigrationsAsync); + } + + protected virtual async Task LockAndApplyDatabaseMigrationsAsync() + { + await using (var handle = await DistributedLock.TryAcquireAsync("Migration_" + DatabaseName)) + { + Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}..."); + + if (handle is null) + { + Logger.LogInformation($"Handle is null because of the locking for : {DatabaseName}"); + return; + } + + using (CurrentTenant.Change(null)) + { + // Create database tables if needed + using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) + { + var dbContext = await ServiceProvider + .GetRequiredService>() + .GetDbContextAsync(); + + var pendingMigrations = await dbContext + .Database + .GetPendingMigrationsAsync(); + + if (pendingMigrations.Any()) + { + await dbContext.Database.MigrateAsync(); + } + + await uow.CompleteAsync(); + } + } + + Logger.LogInformation($"Lock is released for db migration and seeding on database named: {DatabaseName}..."); + } + } + + public async Task TryAsync(Func task, int retryCount = 3) + { + try + { + await task(); + } + catch (Exception ex) + { + retryCount--; + + if (retryCount <= 0) + { + throw; + } + + Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {retryCount} times more. Exception:\n{ex.Message}"); + + await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + + await TryAsync(task, retryCount); + } + } +} \ No newline at end of file From 9eea31b2bbd3db8ab2f736c4b1aa1625d8412f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 16 Jun 2023 11:25:03 +0300 Subject: [PATCH 02/14] Finalized initial runtime database migrations for ef core --- .../Abp/Data/AppliedDatabaseMigrationsEto.cs | 12 ++ .../Data/DatabaseMigrationsAvailableEto.cs | 11 - .../DatabaseMigrationEventHandlerBase.cs | 191 ++++++++++++++---- ...s => EfCoreRuntimeDatabaseMigratorBase.cs} | 84 +++++--- 4 files changed, 226 insertions(+), 72 deletions(-) create mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs delete mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs rename framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/{PendingEfCoreMigrationsChecker.cs => EfCoreRuntimeDatabaseMigratorBase.cs} (51%) diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs new file mode 100644 index 0000000000..739571e241 --- /dev/null +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs @@ -0,0 +1,12 @@ +using System; +using Volo.Abp.EventBus; + +namespace Volo.Abp.Data; + +[Serializable] +[EventName("abp.data.applied_database_migrations")] +public class AppliedDatabaseMigrationsEto +{ + public string DatabaseName { get; set; } + public Guid? TenantId { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs deleted file mode 100644 index 62eff20b6e..0000000000 --- a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Volo.Abp.EventBus; - -namespace Volo.Abp.Data; - -[Serializable] -[EventName("abp.data.database_migrations_available")] -public class DatabaseMigrationsAvailableEto -{ - public string DatabaseName { get; set; } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs index 3c196d0b6e..ce7e21dc0a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs @@ -14,25 +14,42 @@ using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore.Migrations; -public abstract class DatabaseMigrationEventHandlerBase : ITransientDependency +public abstract class DatabaseMigrationEventHandlerBase : + IDistributedEventHandler, + IDistributedEventHandler, + IDistributedEventHandler, + ITransientDependency where TDbContext : DbContext, IEfCoreDbContext { - protected const string TryCountPropertyName = "TryCount"; - protected const int MaxEventTryCount = 3; + protected string DatabaseName { get; } + + protected const string TryCountPropertyName = "__TryCount"; + + protected int MaxEventTryCount { get; set; } = 3; + + /// + /// As milliseconds. + /// + protected int MinValueToWaitOnFailure { get; set; } = 5000; + + /// + /// As milliseconds. + /// + protected int MaxValueToWaitOnFailure { get; set; } = 15000; + protected ICurrentTenant CurrentTenant { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } protected ITenantStore TenantStore { get; } protected IDistributedEventBus DistributedEventBus { get; } protected ILogger> Logger { get; } - protected string DatabaseName { get; } protected DatabaseMigrationEventHandlerBase( - ILoggerFactory loggerFactory, + string databaseName, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, ITenantStore tenantStore, IDistributedEventBus distributedEventBus, - string databaseName) + ILoggerFactory loggerFactory) { CurrentTenant = currentTenant; UnitOfWorkManager = unitOfWorkManager; @@ -43,6 +60,120 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient Logger = loggerFactory.CreateLogger>(); } + public virtual async Task HandleEventAsync(ApplyDatabaseMigrationsEto eventData) + { + if (eventData.DatabaseName != DatabaseName) + { + return; + } + + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.TenantId); + await SeedAsync(eventData.TenantId); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.TenantId + } + ); + } + } + catch (Exception ex) + { + await HandleErrorOnApplyDatabaseMigrationAsync(eventData, ex); + } + + await AfterApplyDatabaseMigrations(eventData, schemaMigrated); + } + + protected virtual Task AfterApplyDatabaseMigrations(ApplyDatabaseMigrationsEto eventData, bool schemaMigrated) + { + return Task.CompletedTask; + } + + public virtual async Task HandleEventAsync(TenantCreatedEto eventData) + { + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); + await SeedAsync(eventData.Id); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.Id + } + ); + } + } + catch (Exception ex) + { + await HandleErrorTenantCreatedAsync(eventData, ex); + } + + await AfterTenantCreated(eventData, schemaMigrated); + } + + protected virtual Task AfterTenantCreated(TenantCreatedEto eventData, bool schemaMigrated) + { + return Task.CompletedTask; + } + + public virtual async Task HandleEventAsync(TenantConnectionStringUpdatedEto eventData) + { + if (eventData.ConnectionStringName != DatabaseName && + eventData.ConnectionStringName != Volo.Abp.Data.ConnectionStrings.DefaultConnectionStringName || + eventData.NewValue.IsNullOrWhiteSpace()) + { + return; + } + + var schemaMigrated = false; + try + { + schemaMigrated = await MigrateDatabaseSchemaAsync(eventData.Id); + await SeedAsync(eventData.Id); + + if (schemaMigrated) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = eventData.Id + } + ); + } + } + catch (Exception ex) + { + await HandleErrorTenantConnectionStringUpdatedAsync(eventData, ex); + } + + await AfterTenantConnectionStringUpdated(eventData, schemaMigrated); + } + + protected virtual Task AfterTenantConnectionStringUpdated(TenantConnectionStringUpdatedEto eventData, + bool schemaMigrated) + { + return Task.CompletedTask; + } + + protected virtual Task SeedAsync(Guid? tenantId) + { + return Task.CompletedTask; + } + /// /// Apply pending EF Core schema migrations to the database. /// Returns true if any migration has applied. @@ -83,7 +214,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient !tenantConfiguration.ConnectionStrings.GetOrDefault(DatabaseName).IsNullOrWhiteSpace()) { //Migrating the tenant database (only if tenant has a separate database) - Logger.LogInformation($"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); + Logger.LogInformation( + $"Migrating separate database of tenant. Database Name = {DatabaseName}, TenantId = {tenantId}"); result = await MigrateDatabaseSchemaWithDbContextAsync(); } } @@ -102,15 +234,17 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); + Logger.LogWarning( + $"Could not apply database migrations. Re-queueing the operation. TenantId = {eventData.TenantId}, Database Name = {eventData.DatabaseName}."); Logger.LogException(exception, LogLevel.Warning); - await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); await DistributedEventBus.PublishAsync(eventData); } else { - Logger.LogError($"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); + Logger.LogError( + $"Could not apply database migrations. Canceling the operation. TenantId = {eventData.TenantId}, DatabaseName = {eventData.DatabaseName}."); Logger.LogException(exception); } } @@ -122,7 +256,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogWarning( + $"Could not perform tenant created event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception, LogLevel.Warning); await Task.Delay(RandomHelper.GetRandom(5000, 15000)); @@ -130,7 +265,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient } else { - Logger.LogError($"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogError( + $"Could not perform tenant created event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception); } } @@ -142,7 +278,8 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient var tryCount = IncrementEventTryCount(eventData); if (tryCount <= MaxEventTryCount) { - Logger.LogWarning($"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogWarning( + $"Could not perform tenant connection string updated event. Re-queueing the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception, LogLevel.Warning); await Task.Delay(RandomHelper.GetRandom(5000, 15000)); @@ -150,34 +287,12 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient } else { - Logger.LogError($"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); + Logger.LogError( + $"Could not perform tenant connection string updated event. Canceling the operation. TenantId = {eventData.Id}, TenantName = {eventData.Name}."); Logger.LogException(exception); } } - protected virtual async Task QueueTenantMigrationsAsync() - { - await DistributedEventBus.PublishAsync( - new DatabaseMigrationsAvailableEto - { - DatabaseName = DatabaseName - } - ); - /* - var tenants = await TenantStore.GetListWithSeparateConnectionStringAsync(); - foreach (var tenant in tenants) - { - await DistributedEventBus.PublishAsync( - new ApplyDatabaseMigrationsEto - { - DatabaseName = DatabaseName, - TenantId = tenant.Id - } - ); - } - */ - } - private static int GetEventTryCount(EtoBase eventData) { var tryCountAsString = eventData.Properties.GetOrDefault(TryCountPropertyName); @@ -200,4 +315,4 @@ public abstract class DatabaseMigrationEventHandlerBase : ITransient SetEventTryCount(eventData, count); return count; } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs similarity index 51% rename from framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs rename to framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs index 56680c24e4..9b9719ff1b 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; using Volo.Abp.DistributedLocking; using Volo.Abp.EventBus.Distributed; using Volo.Abp.MultiTenancy; @@ -11,32 +13,45 @@ using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore.Migrations; -public abstract class PendingEfCoreMigrationsChecker where TDbContext : DbContext, IEfCoreDbContext +public abstract class EfCoreRuntimeDatabaseMigratorBase : ITransientDependency + where TDbContext : DbContext, IEfCoreDbContext { + protected int MinValueToWaitOnFailure { get; set; } = 5000; + protected int MaxValueToWaitOnFailure { get; set; } = 15000; + + protected string DatabaseName { get; } + + /// + /// Enabling this might be inefficient if you have many tenants! + /// If disabled (default), tenant databases will be seeded only + /// if there is a schema migration applied to the host database. + /// If enabled, tenant databases will be seeded always on every service startup. + /// + protected bool AlwaysSeedTenantDatabases { get; set; } = false; + protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IServiceProvider ServiceProvider { get; } protected ICurrentTenant CurrentTenant { get; } - protected IDistributedEventBus DistributedEventBus { get; } protected IAbpDistributedLock DistributedLock { get; } - protected ILogger> Logger { get; } - protected string DatabaseName { get; } + protected IDistributedEventBus DistributedEventBus { get; } + protected ILogger> Logger { get; } - protected PendingEfCoreMigrationsChecker( - ILoggerFactory loggerFactory, + protected EfCoreRuntimeDatabaseMigratorBase( + string databaseName, IUnitOfWorkManager unitOfWorkManager, IServiceProvider serviceProvider, ICurrentTenant currentTenant, - IDistributedEventBus distributedEventBus, IAbpDistributedLock abpDistributedLock, - string databaseName) + IDistributedEventBus distributedEventBus, + ILoggerFactory loggerFactory) { + DatabaseName = databaseName; UnitOfWorkManager = unitOfWorkManager; ServiceProvider = serviceProvider; CurrentTenant = currentTenant; - DistributedEventBus = distributedEventBus; DistributedLock = abpDistributedLock; - DatabaseName = databaseName; - Logger = loggerFactory.CreateLogger>(); + DistributedEventBus = distributedEventBus; + Logger = loggerFactory.CreateLogger>(); } public virtual async Task CheckAndApplyDatabaseMigrationsAsync() @@ -46,15 +61,19 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex protected virtual async Task LockAndApplyDatabaseMigrationsAsync() { - await using (var handle = await DistributedLock.TryAcquireAsync("Migration_" + DatabaseName)) - { - Logger.LogInformation($"Lock is acquired for db migration and seeding on database named: {DatabaseName}..."); + Logger.LogInformation($"Trying to acquire the distributed lock for database migration: {DatabaseName}."); + var schemaMigrated = false; + + await using (var handle = await DistributedLock.TryAcquireAsync("DatabaseMigration_" + DatabaseName)) + { if (handle is null) { - Logger.LogInformation($"Handle is null because of the locking for : {DatabaseName}"); + Logger.LogInformation($"Distributed lock could not be acquired for database migration: {DatabaseName}. Operation cancelled."); return; } + + Logger.LogInformation($"Distributed lock is acquired for database migration: {DatabaseName}..."); using (CurrentTenant.Change(null)) { @@ -72,17 +91,36 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex if (pendingMigrations.Any()) { await dbContext.Database.MigrateAsync(); + schemaMigrated = true; } await uow.CompleteAsync(); } } + + await SeedAsync(); - Logger.LogInformation($"Lock is released for db migration and seeding on database named: {DatabaseName}..."); + if (schemaMigrated || AlwaysSeedTenantDatabases) + { + await DistributedEventBus.PublishAsync( + new AppliedDatabaseMigrationsEto + { + DatabaseName = DatabaseName, + TenantId = null + } + ); + } } + + Logger.LogInformation($"Distributed lock has been released for database migration: {DatabaseName}..."); } - - public async Task TryAsync(Func task, int retryCount = 3) + + protected virtual Task SeedAsync() + { + return Task.CompletedTask; + } + + protected virtual async Task TryAsync(Func task, int maxTryCount = 3) { try { @@ -90,18 +128,18 @@ public abstract class PendingEfCoreMigrationsChecker where TDbContex } catch (Exception ex) { - retryCount--; + maxTryCount--; - if (retryCount <= 0) + if (maxTryCount <= 0) { throw; } - Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {retryCount} times more. Exception:\n{ex.Message}"); + Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {maxTryCount} times more. Exception:\n{ex.Message}. Stack Trace:\n{ex.StackTrace}"); - await Task.Delay(RandomHelper.GetRandom(5000, 15000)); + await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); - await TryAsync(task, retryCount); + await TryAsync(task, maxTryCount); } } } \ No newline at end of file From 66efc5c4ee6b34cee9fa6f6224de744a3aa1c2f0 Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 11:41:31 +0300 Subject: [PATCH 03/14] fix github token --- .github/workflows/update-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml index 386b6858b9..d0a295884f 100644 --- a/.github/workflows/update-versions.yml +++ b/.github/workflows/update-versions.yml @@ -28,6 +28,6 @@ jobs: - name: Update latest-versions.json and create PR env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | python .github/scripts/update_versions.py From 1340bcfa75af81af96c132db93b0e51509764327 Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 11:47:56 +0300 Subject: [PATCH 04/14] Create trigger-release.ps1 --- .github/scripts/trigger-release.ps1 | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/scripts/trigger-release.ps1 diff --git a/.github/scripts/trigger-release.ps1 b/.github/scripts/trigger-release.ps1 new file mode 100644 index 0000000000..9aae8f3ea9 --- /dev/null +++ b/.github/scripts/trigger-release.ps1 @@ -0,0 +1,30 @@ +param( + [string]$tag_name, + [string]$prerelease, + [string]$branchName +) + +# GitHub credentials and repository info +$user = "skoc10" +$repo = "my_projects" +$workflow_file_name = "publish-release.yml" +$token = $env:RELEASE_TOKEN + +# Create a header for the API request +$headers = @{ + Authorization = "Bearer $token" + Accept = "application/vnd.github.v3+json" +} + +# Create a body for the API request +$body = @{ + ref = $branchName + inputs = @{ + tag_name = $tag_name + prerelease = $prerelease + branchName = $branchName + } +} | ConvertTo-Json + +# Send the API request to GitHub +Invoke-RestMethod -Method Post -Headers $headers -Body $body -Uri "https://api.github.com/repos/$user/$repo/actions/workflows/$workflow_file_name/dispatches" From 76f4949bd3b2e7c6031a43a1d740c4a87a75cb9f Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 11:47:59 +0300 Subject: [PATCH 05/14] Create publish-release.yml --- .github/workflows/publish-release.yml | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/publish-release.yml diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000000..b929fa6d3c --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,44 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Tag Name' + required: true + prerelease: + description: 'Pre-release?' + required: true + branchName: + description: 'Branch Name' + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.event.inputs.branchName }} + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + with: + tag_name: ${{ github.event.inputs.tag_name }} + release_name: ${{ github.event.inputs.tag_name }} + draft: false + prerelease: ${{ github.event.inputs.prerelease }} + + - name: Checkout code at tag + uses: actions/checkout@v2 + with: + ref: ${{ github.event.inputs.tag_name }} + + - name: Build Project + run: | + # add your build commands here, depending on your project's requirements. + echo "Build project here" From f2bb91e62a67d7c458803761e6b47d42035d55eb Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 14:03:16 +0300 Subject: [PATCH 06/14] Delete trigger-release.ps1 --- .github/scripts/trigger-release.ps1 | 30 ----------------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/scripts/trigger-release.ps1 diff --git a/.github/scripts/trigger-release.ps1 b/.github/scripts/trigger-release.ps1 deleted file mode 100644 index 9aae8f3ea9..0000000000 --- a/.github/scripts/trigger-release.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param( - [string]$tag_name, - [string]$prerelease, - [string]$branchName -) - -# GitHub credentials and repository info -$user = "skoc10" -$repo = "my_projects" -$workflow_file_name = "publish-release.yml" -$token = $env:RELEASE_TOKEN - -# Create a header for the API request -$headers = @{ - Authorization = "Bearer $token" - Accept = "application/vnd.github.v3+json" -} - -# Create a body for the API request -$body = @{ - ref = $branchName - inputs = @{ - tag_name = $tag_name - prerelease = $prerelease - branchName = $branchName - } -} | ConvertTo-Json - -# Send the API request to GitHub -Invoke-RestMethod -Method Post -Headers $headers -Body $body -Uri "https://api.github.com/repos/$user/$repo/actions/workflows/$workflow_file_name/dispatches" From 98112e68118a31858315affa5d91f41f2c8d636a Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 15:32:12 +0300 Subject: [PATCH 07/14] Update update-versions.yml --- .github/workflows/update-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml index d0a295884f..386b6858b9 100644 --- a/.github/workflows/update-versions.yml +++ b/.github/workflows/update-versions.yml @@ -28,6 +28,6 @@ jobs: - name: Update latest-versions.json and create PR env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | python .github/scripts/update_versions.py From dbb42fee828a0e1860b6e8d5cd198b109606ea85 Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 15:42:41 +0300 Subject: [PATCH 08/14] Update update_versions.py --- .github/scripts/update_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/update_versions.py b/.github/scripts/update_versions.py index 843e80a3a6..117ba2dcd3 100644 --- a/.github/scripts/update_versions.py +++ b/.github/scripts/update_versions.py @@ -45,7 +45,7 @@ def create_pr(): pr.create_review_request(reviewers=["ebicoglu", "gizemmutukurt", "skoc10"]) -if __name__ == "__dev__": +if __name__ == "__main__": should_create_pr = update_latest_versions() if should_create_pr: create_pr() From 4f2513e62d6a35f62509c6551916cade68250f43 Mon Sep 17 00:00:00 2001 From: Salih Date: Fri, 16 Jun 2023 15:46:06 +0300 Subject: [PATCH 09/14] Update Abp Release Log Localizations --- .../AbpIoLocalization/Base/Localization/Resources/en.json | 4 +++- .../Commercial/Localization/Resources/en.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index 87347f21ce..ed84e0bc5c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -212,6 +212,8 @@ "SpringCampaign": "Welcome
Spring Sale!", "SpringCampaign2": "Limited
Time Offer!
", "AboutUs": "About Us", - "HowItWorks": "How it works?" + "HowItWorks": "How it works?", + "ReleaseNotes": "Release Notes", + "DetailedChangeNotes" : "Detailed change notes" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 1ba3124e02..6d029ee428 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -844,7 +844,7 @@ "BlazoriseSupportExplanation5": "You can post your questions on the support website and generate a product token for your application.", "AbpLiveTrainingPackages": "ABP Live Training Packages", "Releases": "Releases", - "ReleasesDescription": "ABP Commercial releases and their changelogs.", + "ReleasesDescription": "Release logs of Abp Commercial.", "ReleaseDate": "Release Date", "Labels": "Labels", "PreRelease": "Pre-release", From 7a8fa646860d0955c3ab329b33cedbc5f108cfca Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 16:16:59 +0300 Subject: [PATCH 10/14] Update update-versions.yml --- .github/workflows/update-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml index 386b6858b9..d0a295884f 100644 --- a/.github/workflows/update-versions.yml +++ b/.github/workflows/update-versions.yml @@ -28,6 +28,6 @@ jobs: - name: Update latest-versions.json and create PR env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | python .github/scripts/update_versions.py From 04d7874b51aa0ce34bd6b3456acd9710effe2d31 Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Fri, 16 Jun 2023 16:22:49 +0300 Subject: [PATCH 11/14] Update en.json --- .../AbpIoLocalization/Commercial/Localization/Resources/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 6d029ee428..cd76e9e134 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -844,7 +844,7 @@ "BlazoriseSupportExplanation5": "You can post your questions on the support website and generate a product token for your application.", "AbpLiveTrainingPackages": "ABP Live Training Packages", "Releases": "Releases", - "ReleasesDescription": "Release logs of Abp Commercial.", + "ReleasesDescription": "Release logs of ABP Commercial.", "ReleaseDate": "Release Date", "Labels": "Labels", "PreRelease": "Pre-release", From 82d3612637641c02d269309d28621941cc736898 Mon Sep 17 00:00:00 2001 From: selmankoc Date: Fri, 16 Jun 2023 16:56:25 +0300 Subject: [PATCH 12/14] added try/except --- .github/scripts/update_versions.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/scripts/update_versions.py b/.github/scripts/update_versions.py index 117ba2dcd3..83b3b9ef74 100644 --- a/.github/scripts/update_versions.py +++ b/.github/scripts/update_versions.py @@ -39,14 +39,16 @@ def create_pr(): branch=branch_name, ) - pr = repo.create_pull(title="Update latest-versions.json", - body="Automated PR to update the latest-versions.json file.", - head=branch_name, base="dev") + try: + pr = repo.create_pull(title="Update latest-versions.json", + body="Automated PR to update the latest-versions.json file.", + head=branch_name, base="dev") + except Exception as e: + print(f"Error while creating PR: {e}") - pr.create_review_request(reviewers=["ebicoglu", "gizemmutukurt", "skoc10"]) + pr.create_review_request(reviewers=["gizemmutukurt"]) if __name__ == "__main__": should_create_pr = update_latest_versions() if should_create_pr: create_pr() - From 7740f5fa0e1f201f29a69cc4ce9c9dd11d073203 Mon Sep 17 00:00:00 2001 From: Salih Date: Fri, 16 Jun 2023 18:06:10 +0300 Subject: [PATCH 13/14] Update en.json --- .../AbpIoLocalization/Base/Localization/Resources/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index ed84e0bc5c..d8dd876bed 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -214,6 +214,7 @@ "AboutUs": "About Us", "HowItWorks": "How it works?", "ReleaseNotes": "Release Notes", - "DetailedChangeNotes" : "Detailed change notes" + "DetailedChangeNotes" : "Detailed change notes", + "SeeTrainings": "See Trainings" } } From 5e74b438ec40accce0396f60f47912302f5bc97a Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 19 Jun 2023 10:57:32 +0800 Subject: [PATCH 14/14] Remove double `escapeSelector `. --- .../Pages/SettingManagement/Index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.js b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.js index 07b40c2c38..3c443a4fac 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.js +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.js @@ -19,7 +19,7 @@ processData: false }).done(function (response) { $('#tab-content').children('.tab-pane').removeClass('show').removeClass('active'); - _this.attr('data-bs-target', '#' + $.escapeSelector($.escapeSelector(id))); + _this.attr('data-bs-target', '#' + $.escapeSelector(id)); $('#tab-content').append('
' + response + '
'); }) });