sozsoft-platform/api/src/Sozsoft.Platform.DbMigrator/Migrations/AfterAllMigrationsSqlExecutor.cs
2026-08-17 12:23:47 +03:00

191 lines
6.9 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.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;
/// <summary>
/// Seeds/{host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki .sql dosyaları
/// iki aşamada işlenir:
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> 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.
/// </summary>
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
{
/// <summary>{sql|postgres} altındaki, migration sonrası da çalıştırılacak script klasörünün adı.</summary>
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<PlatformDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConfiguration _configuration;
private readonly ILogger<AfterAllMigrationsSqlExecutor> _logger;
public AfterAllMigrationsSqlExecutor(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration,
ILogger<AfterAllMigrationsSqlExecutor> 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<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(
"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);
}
/// <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.';";
}
}