sozsoft-platform/api/src/Sozsoft.Platform.DbMigrator/Migrations/FinalSeeder.cs
2026-10-05 15:58:59 +03:00

195 lines
7.2 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.MultiTenancy;
using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// 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
/// 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
{
/// <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;
// 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;
private readonly IConfiguration _configuration;
private readonly ICurrentTenant _currentTenant;
private readonly ILogger<FinalSeeder> _logger;
public FinalSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration,
ICurrentTenant currentTenant,
ILogger<FinalSeeder> logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
_configuration = configuration;
_currentTenant = currentTenant;
_logger = logger;
}
public async Task ExecuteAsync()
{
// 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)
{
_logger.LogInformation("No execute script found, skipping FinalSeeder.");
return;
}
_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();
dbContext.Database.SetCommandTimeout(CommandTimeout);
_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(
"FinalSeeder completed with {FailedCount} failed procedure(s): {Failed}",
failed.Count,
string.Join(", ", failed));
}
else
{
_logger.LogInformation("FinalSeeder completed. {Count} procedure(s) executed.", sqlFiles.Length);
}
}
private IEnumerable<string> GetScriptDirectoryPaths()
{
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
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.';";
}
}