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

196 lines
7.2 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;
2026-10-05 12:58:59 +00:00
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
2026-10-05 12:58:59 +00:00
/// Seeds/{sql|postgres}/execute klasöründeki .sql dosyaları iki aşamada işlenir:
/// 1) Migration/seed sırasında <see cref="SqlDataSeeder"/> tarafından ç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
2026-10-05 12:58:59 +00:00
/// procedure'ü çağıranın seçtiği bağlamda (host ya da kendi veritabanı olan tenant) varlığı
/// doğrulanarak çalıştırır. {sql|postgres}/host/execute yalnızca host bağlamında işlenir.
/// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir.
/// </summary>
public class FinalSeeder : IFinalSeeder, ITransientDependency
{
2026-08-17 09:23:47 +00:00
/// <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;
2026-10-04 21:31:43 +00:00
// Buradaki procedure'ler bütün ekranlar üzerinden toplu iş yapar (ör. alan sözlüğü); dolu bir
// veritabanında EF'in 30 sn varsayılanını aşar ve migrate sözlüğü boş bırakır.
private static readonly TimeSpan CommandTimeout = TimeSpan.FromMinutes(10);
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;
2026-10-05 12:58:59 +00:00
private readonly ICurrentTenant _currentTenant;
private readonly ILogger<FinalSeeder> _logger;
public FinalSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
2026-08-17 05:56:52 +00:00
IConfiguration configuration,
2026-10-05 12:58:59 +00:00
ICurrentTenant currentTenant,
ILogger<FinalSeeder> logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
2026-08-17 05:56:52 +00:00
_configuration = configuration;
2026-10-05 12:58:59 +00:00
_currentTenant = currentTenant;
_logger = logger;
}
2026-10-05 12:58:59 +00:00
public async Task ExecuteAsync()
{
2026-10-05 12:58:59 +00:00
// Ortak procedure'ler once, host'a ozgu olanlar (host/execute) yalnizca host baglaminda sonra.
var sqlFiles = GetScriptDirectoryPaths()
.Where(Directory.Exists)
.SelectMany(path => Directory.GetFiles(path, "*.sql")
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
.ToArray();
if (sqlFiles.Length == 0)
{
2026-10-05 12:58:59 +00:00
_logger.LogInformation("No execute script found, skipping FinalSeeder.");
return;
}
2026-10-05 12:58:59 +00:00
_logger.LogInformation("FinalSeeder started. {Count} procedure(s) to be executed.", 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();
2026-10-04 21:31:43 +00:00
dbContext.Database.SetCommandTimeout(CommandTimeout);
_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(
"FinalSeeder completed with {FailedCount} failed procedure(s): {Failed}",
failed.Count,
string.Join(", ", failed));
}
else
{
_logger.LogInformation("FinalSeeder completed. {Count} procedure(s) executed.", sqlFiles.Length);
}
}
2026-10-05 12:58:59 +00:00
private IEnumerable<string> GetScriptDirectoryPaths()
{
2026-08-17 09:23:47 +00:00
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
2026-10-05 12:58:59 +00:00
yield return SeedPathResolver.GetPath(_configuration, providerDirectoryName, DirectoryName);
if (_currentTenant.Id == null)
yield return SeedPathResolver.GetPath(_configuration, providerDirectoryName, SeedPathResolver.HostOnlyFolder, 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.';";
}
}