191 lines
6.9 KiB
C#
191 lines
6.9 KiB
C#
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.';";
|
||
}
|
||
}
|