Browse Source

Finalized initial runtime database migrations for ef core

pull/16862/head
Halil İbrahim Kalkan 3 years ago
parent
commit
9eea31b2bb
  1. 12
      framework/src/Volo.Abp.Data/Volo/Abp/Data/AppliedDatabaseMigrationsEto.cs
  2. 11
      framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs
  3. 191
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/DatabaseMigrationEventHandlerBase.cs
  4. 84
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs

12
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; }
}

11
framework/src/Volo.Abp.Data/Volo/Abp/Data/DatabaseMigrationsAvailableEto.cs

@ -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; }
}

191
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<TDbContext> : ITransientDependency
public abstract class DatabaseMigrationEventHandlerBase<TDbContext> :
IDistributedEventHandler<TenantCreatedEto>,
IDistributedEventHandler<TenantConnectionStringUpdatedEto>,
IDistributedEventHandler<ApplyDatabaseMigrationsEto>,
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;
/// <summary>
/// As milliseconds.
/// </summary>
protected int MinValueToWaitOnFailure { get; set; } = 5000;
/// <summary>
/// As milliseconds.
/// </summary>
protected int MaxValueToWaitOnFailure { get; set; } = 15000;
protected ICurrentTenant CurrentTenant { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected ITenantStore TenantStore { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected ILogger<DatabaseMigrationEventHandlerBase<TDbContext>> 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<TDbContext> : ITransient
Logger = loggerFactory.CreateLogger<DatabaseMigrationEventHandlerBase<TDbContext>>();
}
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;
}
/// <summary>
/// Apply pending EF Core schema migrations to the database.
/// Returns true if any migration has applied.
@ -83,7 +214,8 @@ public abstract class DatabaseMigrationEventHandlerBase<TDbContext> : 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<TDbContext> : 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<TDbContext> : 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<TDbContext> : 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<TDbContext> : 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<TDbContext> : 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<TDbContext> : ITransient
SetEventTryCount(eventData, count);
return count;
}
}
}

84
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/PendingEfCoreMigrationsChecker.cs → 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<TDbContext> where TDbContext : DbContext, IEfCoreDbContext
public abstract class EfCoreRuntimeDatabaseMigratorBase<TDbContext> : ITransientDependency
where TDbContext : DbContext, IEfCoreDbContext
{
protected int MinValueToWaitOnFailure { get; set; } = 5000;
protected int MaxValueToWaitOnFailure { get; set; } = 15000;
protected string DatabaseName { get; }
/// <summary>
/// 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.
/// </summary>
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<PendingEfCoreMigrationsChecker<TDbContext>> Logger { get; }
protected string DatabaseName { get; }
protected IDistributedEventBus DistributedEventBus { get; }
protected ILogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>> 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<PendingEfCoreMigrationsChecker<TDbContext>>();
DistributedEventBus = distributedEventBus;
Logger = loggerFactory.CreateLogger<EfCoreRuntimeDatabaseMigratorBase<TDbContext>>();
}
public virtual async Task CheckAndApplyDatabaseMigrationsAsync()
@ -46,15 +61,19 @@ public abstract class PendingEfCoreMigrationsChecker<TDbContext> 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<TDbContext> 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> task, int retryCount = 3)
protected virtual Task SeedAsync()
{
return Task.CompletedTask;
}
protected virtual async Task TryAsync(Func<Task> task, int maxTryCount = 3)
{
try
{
@ -90,18 +128,18 @@ public abstract class PendingEfCoreMigrationsChecker<TDbContext> 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);
}
}
}
Loading…
Cancel
Save