2026-08-17 05:56:52 +00:00
|
|
|
|
using System;
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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;
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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;
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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).
|
2026-08-11 19:55:20 +00:00
|
|
|
|
/// 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.
|
2026-08-11 19:55:20 +00:00
|
|
|
|
/// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir.
|
|
|
|
|
|
/// </summary>
|
2026-09-18 06:03:19 +00:00
|
|
|
|
public class FinalSeeder : IFinalSeeder, ITransientDependency
|
2026-08-11 19:55:20 +00:00
|
|
|
|
{
|
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-08-11 19:55:20 +00:00
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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;
|
2026-09-18 06:03:19 +00:00
|
|
|
|
private readonly ILogger<FinalSeeder> _logger;
|
2026-08-11 19:55:20 +00:00
|
|
|
|
|
2026-09-18 06:03:19 +00:00
|
|
|
|
public FinalSeeder(
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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,
|
2026-09-18 06:03:19 +00:00
|
|
|
|
ILogger<FinalSeeder> logger)
|
2026-08-11 19:55:20 +00:00
|
|
|
|
{
|
|
|
|
|
|
_dbContextProvider = dbContextProvider;
|
|
|
|
|
|
_unitOfWorkManager = unitOfWorkManager;
|
2026-08-17 05:56:52 +00:00
|
|
|
|
_configuration = configuration;
|
2026-10-05 12:58:59 +00:00
|
|
|
|
_currentTenant = currentTenant;
|
2026-08-11 19:55:20 +00:00
|
|
|
|
_logger = logger;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-05 12:58:59 +00:00
|
|
|
|
public async Task ExecuteAsync()
|
2026-08-11 19:55:20 +00:00
|
|
|
|
{
|
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))
|
2026-08-11 19:55:20 +00:00
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
if (sqlFiles.Length == 0)
|
|
|
|
|
|
{
|
2026-10-05 12:58:59 +00:00
|
|
|
|
_logger.LogInformation("No execute script found, skipping FinalSeeder.");
|
2026-08-11 19:55:20 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-05 12:58:59 +00:00
|
|
|
|
_logger.LogInformation("FinalSeeder started. {Count} procedure(s) to be executed.", sqlFiles.Length);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
2026-08-12 12:20:00 +00:00
|
|
|
|
"Executing procedure {Schema}.{Procedure}",
|
2026-08-11 19:55:20 +00:00
|
|
|
|
schemaName,
|
2026-08-12 12:20:00 +00:00
|
|
|
|
procedureName);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
|
|
|
|
|
|
await dbContext.Database.ExecuteSqlRawAsync(BuildExecuteScript(schemaName, procedureName));
|
|
|
|
|
|
|
|
|
|
|
|
await uow.CompleteAsync();
|
|
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation(
|
2026-08-12 12:20:00 +00:00
|
|
|
|
"Executed procedure {Schema}.{Procedure}",
|
2026-08-11 19:55:20 +00:00
|
|
|
|
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);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (failed.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(
|
2026-09-18 06:03:19 +00:00
|
|
|
|
"FinalSeeder completed with {FailedCount} failed procedure(s): {Failed}",
|
2026-08-11 19:55:20 +00:00
|
|
|
|
failed.Count,
|
|
|
|
|
|
string.Join(", ", failed));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-09-18 06:03:19 +00:00
|
|
|
|
_logger.LogInformation("FinalSeeder completed. {Count} procedure(s) executed.", sqlFiles.Length);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-05 12:58:59 +00:00
|
|
|
|
private IEnumerable<string> GetScriptDirectoryPaths()
|
2026-08-11 19:55:20 +00:00
|
|
|
|
{
|
2026-08-17 09:23:47 +00:00
|
|
|
|
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
|
|
|
|
|
|
DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
|
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);
|
2026-08-11 19:55:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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.';";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|