using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Sozsoft.Platform.EntityFrameworkCore; using Volo.Abp.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Uow; using static Sozsoft.Settings.SettingsConsts; namespace Sozsoft.Platform.Data.Seeds; /// /// Seeds/{host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki .sql dosyaları /// iki aşamada işlenir: /// 1) Migration/seed sırasında tarafından, kapsam klasörüyle /// (host/tenants/{tenantId}) aynı bağlamda çalıştırılır (nesne oluşturulur/güncellenir). /// 2) Tüm migration ve seed işlemleri bittikten sonra bu sınıf, dosya adından türetilen stored /// procedure'ü ilgili bağlamda (host ya da tenant) varlığı doğrulanarak çalıştırır. /// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir. /// public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency { /// {sql|postgres} altındaki, migration sonrası da çalıştırılacak script klasörünün adı. public const string DirectoryName = SeedPathResolver.ExecuteFolder; private static readonly Regex SafeIdentifierRegex = new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled); private readonly IDbContextProvider _dbContextProvider; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IConfiguration _configuration; private readonly ILogger _logger; public AfterAllMigrationsSqlExecutor( IDbContextProvider dbContextProvider, IUnitOfWorkManager unitOfWorkManager, IConfiguration configuration, ILogger logger) { _dbContextProvider = dbContextProvider; _unitOfWorkManager = unitOfWorkManager; _configuration = configuration; _logger = logger; } public async Task ExecuteAsync(Guid? tenantId = null) { var directoryPath = GetScriptDirectoryPath(tenantId); if (!Directory.Exists(directoryPath)) { _logger.LogInformation("{Directory} directory not found, skipping AfterAllMigrationsSqlExecutor.", directoryPath); return; } var sqlFiles = Directory.GetFiles(directoryPath, "*.sql") .OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase) .ToArray(); if (sqlFiles.Length == 0) { _logger.LogInformation("No .sql files found in {Directory}, skipping AfterAllMigrationsSqlExecutor.", directoryPath); return; } _logger.LogInformation( "AfterAllMigrationsSqlExecutor started for {Scope}. {Count} procedure(s) to be executed.", tenantId == null ? "Host" : $"Tenant {tenantId}", sqlFiles.Length); var failed = new List(); foreach (var filePath in sqlFiles) { var objectName = Path.GetFileNameWithoutExtension(filePath); if (!TryResolveObjectName(objectName, out var schemaName, out var procedureName)) { _logger.LogWarning( "Skipped {FileName}: file name is not a valid procedure identifier ([schema.]ProcedureName).", Path.GetFileName(filePath)); continue; } try { using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); var dbContext = await _dbContextProvider.GetDbContextAsync(); _logger.LogInformation( "Executing procedure {Schema}.{Procedure}", schemaName, procedureName); await dbContext.Database.ExecuteSqlRawAsync(BuildExecuteScript(schemaName, procedureName)); await uow.CompleteAsync(); _logger.LogInformation( "Executed procedure {Schema}.{Procedure}", schemaName, procedureName); } catch (Exception ex) { failed.Add($"{schemaName}.{procedureName}"); _logger.LogError(ex, "Procedure failed: {Schema}.{Procedure}", schemaName, procedureName); } } if (failed.Count > 0) { _logger.LogWarning( "AfterAllMigrationsSqlExecutor completed with {FailedCount} failed procedure(s): {Failed}", failed.Count, string.Join(", ", failed)); } else { _logger.LogInformation("AfterAllMigrationsSqlExecutor completed. {Count} procedure(s) executed.", sqlFiles.Length); } } private string GetScriptDirectoryPath(Guid? tenantId) { var providerDirectoryName = SeedPathResolver.GetProviderFolderName( DefaultDatabaseProvider == DatabaseProvider.PostgreSql); return SeedPathResolver.GetScopePath(_configuration, tenantId, providerDirectoryName, DirectoryName); } /// /// Dosya adını [schema.]ProcedureName olarak çözer ve her iki parçayı da beyaz liste ile doğrular. /// Doğrulanmayan hiçbir ad SQL metnine yazılmaz. /// private static bool TryResolveObjectName(string fileName, out string schemaName, out string procedureName) { var defaultSchema = DefaultDatabaseProvider == DatabaseProvider.PostgreSql ? "public" : "dbo"; schemaName = defaultSchema; procedureName = fileName; var parts = fileName.Split('.'); if (parts.Length == 2) { schemaName = parts[0]; procedureName = parts[1]; } else if (parts.Length > 2) { return false; } return SafeIdentifierRegex.IsMatch(schemaName) && SafeIdentifierRegex.IsMatch(procedureName); } private static string BuildExecuteScript(string schemaName, string procedureName) { if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql) { return $@" DO $$ BEGIN IF EXISTS ( SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = '{schemaName}' AND p.proname = '{procedureName}' AND p.prokind = 'p' ) THEN CALL ""{schemaName}"".""{procedureName}""(); ELSE RAISE NOTICE 'Procedure {schemaName}.{procedureName} not found, skipped.'; END IF; END $$;"; } return $@" IF OBJECT_ID(N'[{schemaName}].[{procedureName}]', N'P') IS NOT NULL EXEC [{schemaName}].[{procedureName}]; ELSE PRINT N'Procedure [{schemaName}].[{procedureName}] not found, skipped.';"; } }