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

272 lines
11 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.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<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<IFinalSeeder> _finalSeeders;
private readonly IEnumerable<IWizardDataSeeder> _wizardDataSeeders;
private readonly IListFormSeedDataApplier _listFormSeedDataApplier;
private readonly DistributedCacheCleaner _cacheCleaner;
public PlatformDbMigrationService(
IDataSeeder dataSeeder,
IEnumerable<IPlatformDbSchemaMigrator> dbSchemaMigrators,
ITenantRepository tenantRepository,
ICurrentTenant currentTenant,
IConfiguration configuration,
IEnumerable<IFinalSeeder> finalSeeders,
IEnumerable<IWizardDataSeeder> wizardDataSeeders,
IListFormSeedDataApplier listFormSeedDataApplier,
DistributedCacheCleaner cacheCleaner)
{
_dataSeeder = dataSeeder;
_dbSchemaMigrators = dbSchemaMigrators;
_tenantRepository = tenantRepository;
_currentTenant = currentTenant;
_configuration = configuration;
_finalSeeders = finalSeeders;
_wizardDataSeeders = wizardDataSeeders;
_listFormSeedDataApplier = listFormSeedDataApplier;
_cacheCleaner = cacheCleaner;
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")}");
await ClearCacheAsync();
var tenantSeedId = _configuration.GetValue<Guid?>(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<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, 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...");
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
private async Task ExecuteFinalSeedersAsync(List<Tenant> 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);
}
var context = new DataSeedContext(tenant?.Id)
.WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, adminEmail)
.WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, adminPassword)
.WithProperty(IdentityDataSeedContributor.AdminUserNamePropertyName, adminEmail)
.WithProperty(SeedFileScope.PropertyName, applySeedFiles)
.WithProperty(SeedFileScope.CompletesPendingPropertyName, completesPending);
await _dataSeeder.SeedAsync(context);
// Wizard ekranlari contributor'larin kayitlarina (veri kaynagi, ListForm) dayanir ve dosya basina
// kendi transaction'inda kurulur; contributor UoW'si commit edilmeden calisamaz (IWizardDataSeeder).
foreach (var wizardDataSeeder in _wizardDataSeeders)
{
await wizardDataSeeder.SeedAsync(context);
}
// Veri seed dosyalari ListForm tanimlarina ve tablolara dayandigi icin butun
// contributor'lar ve wizard kurulumu bittikten sonra uygulanir; ABP contributor sirasi garanti degildir.
if (applySeedFiles)
{
await _listFormSeedDataApplier.ApplyAsync(tenant?.Id);
}
}
}