sozsoft-platform/api/src/Sozsoft.Platform.Domain/Data/PlatformDbMigrationService.cs
2026-08-12 15:20:00 +03:00

291 lines
9.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StackExchange.Redis;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.TenantManagement;
namespace Sozsoft.Platform.Data;
public class PlatformDbMigrationService : ITransientDependency
{
public ILogger<PlatformDbMigrationService> Logger { get; set; }
private readonly IDataSeeder _dataSeeder;
private readonly IEnumerable<IPlatformDbSchemaMigrator> _dbSchemaMigrators;
private readonly ITenantRepository _tenantRepository;
private readonly ICurrentTenant _currentTenant;
private readonly IConfiguration _configuration;
private readonly IEnumerable<IAfterAllMigrationsSqlExecutor> _afterAllMigrationsSqlExecutors;
public PlatformDbMigrationService(
IDataSeeder dataSeeder,
IEnumerable<IPlatformDbSchemaMigrator> dbSchemaMigrators,
ITenantRepository tenantRepository,
ICurrentTenant currentTenant,
IConfiguration configuration,
IEnumerable<IAfterAllMigrationsSqlExecutor> afterAllMigrationsSqlExecutors)
{
_dataSeeder = dataSeeder;
_dbSchemaMigrators = dbSchemaMigrators;
_tenantRepository = tenantRepository;
_currentTenant = currentTenant;
_configuration = configuration;
_afterAllMigrationsSqlExecutors = afterAllMigrationsSqlExecutors;
Logger = NullLogger<PlatformDbMigrationService>.Instance;
}
public async Task MigrateAsync()
{
var seed = _configuration.GetValue("Seed", false);
Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}");
//Clear Cache
var redis = ConnectionMultiplexer.Connect(_configuration["Redis:Configuration"]!);
var db = redis.GetDatabase();
db.Execute("FLUSHALL");
var initialMigrationAdded = AddInitialMigrationIfNotExist();
if (initialMigrationAdded)
{
return;
}
Logger.LogInformation("Started database migrations...");
await MigrateDatabaseSchemaAsync();
if (seed)
{
await SeedDataAsync();
}
Logger.LogInformation($"Successfully completed host database migrations.");
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var migratedDatabaseSchemas = new HashSet<string>();
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
if (tenant.ConnectionStrings.Any())
{
var tenantConnectionStrings = tenant.ConnectionStrings
.Select(x => x.Value)
.ToList();
if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings))
{
await MigrateDatabaseSchemaAsync(tenant);
migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings);
}
}
if (seed)
{
await SeedDataAsync(tenant);
}
}
Logger.LogInformation($"Successfully completed {tenant.Name} tenant database migrations.");
}
Logger.LogInformation("Successfully completed all database migrations.");
await ExecuteAfterAllMigrationsSqlAsync(tenants);
Logger.LogInformation("You can safely end this process...");
}
/// <summary>
/// Tüm host ve tenant migration/seed adımları bittikten sonra, {HostData|TenantData}/ExecuteData
/// klasöründeki script'lerin dosya adından türeyen procedure'lerini ilgili bağlamda çalıştırır.
/// TenantData klasörü yoksa tenant adımı sessizce atlanır.
/// </summary>
private async Task ExecuteAfterAllMigrationsSqlAsync(List<Tenant> tenants)
{
if (!_afterAllMigrationsSqlExecutors.Any())
{
Logger.LogInformation("No SQL executor registered, skipping ExecuteData scripts.");
return;
}
Logger.LogInformation(
"Executing SQL scripts for host and {TenantCount} tenant(s)...",
tenants.Count);
using (_currentTenant.Change(null))
{
foreach (var executor in _afterAllMigrationsSqlExecutors)
{
Logger.LogInformation("Running SQL scripts for host...");
await executor.ExecuteAsync();
Logger.LogInformation("Completed SQL scripts for host.");
}
}
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
foreach (var executor in _afterAllMigrationsSqlExecutors)
{
Logger.LogInformation(
"Running SQL scripts for {TenantName} tenant...",
tenant.Name);
await executor.ExecuteAsync(tenant.Id);
Logger.LogInformation(
"Completed SQL scripts for {TenantName} tenant.",
tenant.Name);
}
}
}
Logger.LogInformation("Successfully completed all SQL scripts.");
}
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation(
$"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
foreach (var migrator in _dbSchemaMigrators)
{
await migrator.MigrateAsync();
}
}
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue)
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue)
);
}
private bool AddInitialMigrationIfNotExist()
{
try
{
if (!DbMigrationsProjectExists())
{
return false;
}
}
catch (Exception)
{
return false;
}
try
{
if (!MigrationsFolderExists())
{
AddInitialMigration();
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
Logger.LogWarning("Couldn't determinate if any migrations exist : " + e.Message);
return false;
}
}
private bool DbMigrationsProjectExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null;
}
private bool MigrationsFolderExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return dbMigrationsProjectFolder != null && Directory.Exists(Path.Combine(dbMigrationsProjectFolder, "Migrations"));
}
private void AddInitialMigration()
{
Logger.LogInformation("Creating initial migration...");
string argumentPrefix;
string fileName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
argumentPrefix = "-c";
fileName = "/bin/bash";
}
else
{
argumentPrefix = "/C";
fileName = "cmd.exe";
}
var procStartInfo = new ProcessStartInfo(fileName,
$"{argumentPrefix} \"abp create-migration-and-run-migrator \"{GetEntityFrameworkCoreProjectFolderPath()}\"\""
);
try
{
Process.Start(procStartInfo);
}
catch (Exception)
{
throw new Exception("Couldn't run ABP CLI...");
}
}
private string? GetEntityFrameworkCoreProjectFolderPath()
{
var slnDirectoryPath = GetSolutionDirectoryPath();
if (slnDirectoryPath == null)
{
throw new Exception("Solution folder not found!");
}
var srcDirectoryPath = Path.Combine(slnDirectoryPath, "src");
return Directory.GetDirectories(srcDirectoryPath)
.FirstOrDefault(d => d.EndsWith(".EntityFrameworkCore"));
}
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}
}
return null;
}
}