using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Sozsoft.Languages.Caching; using Sozsoft.Platform.Data.Seeds; 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 Logger { get; set; } private readonly IDataSeeder _dataSeeder; private readonly IEnumerable _dbSchemaMigrators; private readonly ITenantRepository _tenantRepository; private readonly ICurrentTenant _currentTenant; private readonly IConfiguration _configuration; private readonly IEnumerable _finalSeeders; private readonly IListFormSeedDataApplier _listFormSeedDataApplier; private readonly DistributedCacheCleaner _cacheCleaner; public PlatformDbMigrationService( IDataSeeder dataSeeder, IEnumerable dbSchemaMigrators, ITenantRepository tenantRepository, ICurrentTenant currentTenant, IConfiguration configuration, IEnumerable finalSeeders, IListFormSeedDataApplier listFormSeedDataApplier, DistributedCacheCleaner cacheCleaner) { _dataSeeder = dataSeeder; _dbSchemaMigrators = dbSchemaMigrators; _tenantRepository = tenantRepository; _currentTenant = currentTenant; _configuration = configuration; _finalSeeders = finalSeeders; _listFormSeedDataApplier = listFormSeedDataApplier; _cacheCleaner = cacheCleaner; Logger = NullLogger.Instance; } public async Task MigrateAsync() { var seed = _configuration.GetValue("Seed", false); Logger.LogInformation($"Migrating database. Seed: {seed}. Env: {Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}"); await ClearCacheAsync(); var tenantSeedId = _configuration.GetValue(TenantSeedOptions.TenantIdKey); if (tenantSeedId.HasValue) { await SeedSingleTenantAsync(tenantSeedId.Value); return; } Logger.LogInformation("Started database migrations..."); await MigrateDatabaseSchemaAsync(); var tenants = await _tenantRepository.GetListAsync(includeDetails: true); // Bekleyen wizard kurulumu dosyaları uygulayan son veritabanında kapanır; host'ta kapansaydı // ekranın eski sürümü kurulu olan ayrı veritabanlı tenant onu "kurulu" sayıp güncellemezdi. var lastSeedFileTenant = tenants.LastOrDefault(SeedFileScope.AppliesTo); if (seed) { await SeedDataAsync(completesPending: lastSeedFileTenant == null); } Logger.LogInformation($"Successfully completed host database migrations."); var migratedDatabaseSchemas = new HashSet(); 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, completesPending: tenant == lastSeedFileTenant); } } Logger.LogInformation($"Successfully completed {tenant.Name} tenant database migrations."); } Logger.LogInformation("Successfully completed all database migrations."); await ExecuteFinalSeedersAsync(tenants, includeHost: true); // Seed; menu, yetki ve dil metni ekler. Calisan uygulama bu arada eski halleri onbellege // almis olabilir (ornegin wizard'in yeni menusu); migrate sonrasinda da temizlenir. if (seed) await ClearCacheAsync(); Logger.LogInformation("You can safely end this process..."); } /// /// Tenants ekranının "Seed" sekmesi: yalnızca verilen tenant'ın şemasını ve seed'ini /// tam migrate'le aynı adımlarla çalıştırır. Yönetici bilgisi bu tenant için ekrandan gelir; /// tam migrate'teki varsayılan yönetici kullanılmaz. /// private async Task SeedSingleTenantAsync(Guid tenantId) { var tenant = await _tenantRepository.FindAsync(tenantId, includeDetails: true) ?? throw new InvalidOperationException($"Tenant not found: {tenantId}"); var adminEmail = _configuration[TenantSeedOptions.AdminEmailKey]; var adminPassword = _configuration[TenantSeedOptions.AdminPasswordKey]; if (string.IsNullOrWhiteSpace(adminEmail) || string.IsNullOrWhiteSpace(adminPassword)) { throw new InvalidOperationException("Admin email and password are required for tenant seed."); } Logger.LogInformation("Started {TenantName} tenant migration and seed...", tenant.Name); using (_currentTenant.Change(tenant.Id)) { if (tenant.ConnectionStrings.Any()) { await MigrateDatabaseSchemaAsync(tenant); } // Host ve diğer tenant'lar bu çalışmada kurulmadığı için bekleyen wizard kurulumu açık kalır. await SeedDataAsync(tenant, completesPending: false, adminEmail, adminPassword); } Logger.LogInformation("Successfully completed {TenantName} tenant database migrations.", tenant.Name); await ExecuteFinalSeedersAsync([tenant], includeHost: false); await ClearCacheAsync(); Logger.LogInformation("You can safely end this process..."); } private async Task ClearCacheAsync() { var removed = await _cacheCleaner.ClearAsync(); Logger.LogInformation("Distributed cache cleared. {Count} key(s) removed.", removed); } /// /// Tüm host ve tenant migration/seed adımları bittikten sonra, /// {sql|postgres}/execute klasöründeki script'lerin dosya adından türeyen procedure'lerini /// host'ta ve kendi veritabanı olan her tenant'ta çalıştırır. Host veritabanını paylaşan tenant /// atlanır; procedure o veritabanında host adımında zaten çalışmıştır. /// private async Task ExecuteFinalSeedersAsync(List tenants, bool includeHost) { if (!_finalSeeders.Any()) { Logger.LogInformation("No SQL executor registered, skipping execute scripts."); return; } Logger.LogInformation( "Executing SQL scripts for {Host}{TenantCount} tenant(s)...", includeHost ? "host and " : string.Empty, tenants.Count); if (includeHost) { using (_currentTenant.Change(null)) { foreach (var executor in _finalSeeders) { Logger.LogInformation("Running SQL scripts for host..."); await executor.ExecuteAsync(); Logger.LogInformation("Completed SQL scripts for host."); } } } foreach (var tenant in tenants.Where(SeedFileScope.AppliesTo)) { using (_currentTenant.Change(tenant.Id)) { foreach (var executor in _finalSeeders) { Logger.LogInformation( "Running SQL scripts for {TenantName} tenant...", tenant.Name); await executor.ExecuteAsync(); 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, bool completesPending = true, string adminEmail = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue, string adminPassword = PlatformConsts.AbpIdentity.User.AdminPasswordDefaultValue) { Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed..."); var applySeedFiles = SeedFileScope.AppliesTo(tenant); if (!applySeedFiles) { Logger.LogInformation("{TenantName} tenant shares the host database; seed files are already applied there.", tenant!.Name); } await _dataSeeder.SeedAsync(new DataSeedContext(tenant?.Id) .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, adminEmail) .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, adminPassword) .WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, adminEmail) .WithProperty(SeedFileScope.PropertyName, applySeedFiles) .WithProperty(SeedFileScope.CompletesPendingPropertyName, completesPending) ); // Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun // contributor'lar bittikten sonra uygulanir; ABP contributor sirasi garanti degildir. if (applySeedFiles) { await _listFormSeedDataApplier.ApplyAsync(tenant?.Id); } } }