sozsoft-platform/api/src/Sozsoft.Platform.DbMigrator/Migrations/AfterAllMigrationsSqlExecutor.cs

200 lines
7.4 KiB
C#
Raw Normal View History

2026-08-17 05:56:52 +00:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
2026-08-17 05:56:52 +00:00
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;
/// <summary>
/// Seeds/{SqlData|PostgresData}/{HostData|TenantData}/ExecuteData klasöründeki .sql dosyaları
/// iki aşamada işlenir:
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından, üst klasörle (HostData/TenantData)
/// aynı kapsamda ç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.
/// </summary>
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
{
/// <summary>HostData/TenantData altındaki, migration sonrası da çalıştırılacak script klasörünün adı.</summary>
2026-08-17 05:56:52 +00:00
public const string DirectoryName = SeedPathResolver.ExecuteDataFolder;
/// <summary>Host kapsamındaki script klasörü.</summary>
2026-08-17 05:56:52 +00:00
public const string HostDataDirectoryName = SeedPathResolver.HostDataFolder;
/// <summary>Tenant kapsamındaki script klasörü (henüz kullanılmıyorsa sessizce atlanır).</summary>
2026-08-17 05:56:52 +00:00
public const string TenantDataDirectoryName = SeedPathResolver.TenantDataFolder;
private static readonly Regex SafeIdentifierRegex =
new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled);
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
2026-08-17 05:56:52 +00:00
private readonly IConfiguration _configuration;
private readonly ILogger<AfterAllMigrationsSqlExecutor> _logger;
public AfterAllMigrationsSqlExecutor(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
2026-08-17 05:56:52 +00:00
IConfiguration configuration,
ILogger<AfterAllMigrationsSqlExecutor> logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
2026-08-17 05:56:52 +00:00
_configuration = configuration;
_logger = logger;
}
public async Task ExecuteAsync(Guid? tenantId = null)
{
var scopeDirectoryName = tenantId == null ? HostDataDirectoryName : TenantDataDirectoryName;
var directoryPath = GetScriptDirectoryPath(scopeDirectoryName);
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<string>();
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(
2026-08-12 12:20:00 +00:00
"Executing procedure {Schema}.{Procedure}",
schemaName,
2026-08-12 12:20:00 +00:00
procedureName);
await dbContext.Database.ExecuteSqlRawAsync(BuildExecuteScript(schemaName, procedureName));
await uow.CompleteAsync();
_logger.LogInformation(
2026-08-12 12:20:00 +00:00
"Executed procedure {Schema}.{Procedure}",
schemaName,
procedureName);
}
catch (Exception ex)
{
failed.Add($"{schemaName}.{procedureName}");
2026-08-12 12:20:00 +00:00
_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);
}
}
2026-08-17 05:56:52 +00:00
private string GetScriptDirectoryPath(string scopeDirectoryName)
{
var dataDirectoryName = DefaultDatabaseProvider == DatabaseProvider.PostgreSql
2026-08-17 05:56:52 +00:00
? SeedPathResolver.PostgresDataFolder
: SeedPathResolver.SqlDataFolder;
2026-08-17 05:56:52 +00:00
return SeedPathResolver.GetPath(_configuration, dataDirectoryName, scopeDirectoryName, DirectoryName);
}
/// <summary>
/// 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.
/// </summary>
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.';";
}
}