using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Infrastructure.BackgroundServices;
///
/// سرویس دیپلوی خودکار Stored Procedureها در استارتاپ (Q26).
/// فایلهای .sql از EmbeddedResource خوانده شده، SHA256 محاسبه و با جدول
/// [CMS].[__StoredProcedureVersions] مقایسه میشود.
/// اگر checksum تغییر کرده باشد، SP دوباره اجرا (CREATE OR ALTER) میشود.
/// این سرویس فقط یکبار در استارتاپ اجرا شده و سپس متوقف میشود.
///
public class StoredProcedureDeploymentService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger _logger;
///
/// پیشوند نامفضای EmbeddedResource — فقط فایلهای .sql واقعی (بدون README)
///
private const string ResourcePrefix = "CMSMicroservice.Infrastructure.Persistence.StoredProcedures.";
public StoredProcedureDeploymentService(
IServiceScopeFactory scopeFactory,
ILogger logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// صبر برای آماده شدن دیتابیس و اتمام Migrationها
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
_logger.LogInformation("StoredProcedureDeploymentService started — checking stored procedures...");
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService();
var connection = context.Database.GetDbConnection();
await connection.OpenAsync(stoppingToken);
// ── ساخت جدول ورژن اگر وجود ندارد ──
await EnsureVersionTableAsync(connection, stoppingToken);
// ── خواندن فایلهای SP از Embedded Resources ──
var assembly = Assembly.GetExecutingAssembly();
var resourceNames = assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(ResourcePrefix, StringComparison.OrdinalIgnoreCase)
&& r.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)
&& !r.Contains("README", StringComparison.OrdinalIgnoreCase))
.OrderBy(r => r)
.ToList();
_logger.LogInformation("Found {Count} SQL resources to check", resourceNames.Count);
var deployed = 0;
var skipped = 0;
foreach (var resourceName in resourceNames)
{
if (stoppingToken.IsCancellationRequested) break;
var fileName = resourceName[ResourcePrefix.Length..]; // e.g. "sp_CalculateWeeklyBalances.sql"
var sqlContent = await ReadResourceAsync(assembly, resourceName);
var checksum = ComputeSha256(sqlContent);
var existingChecksum = await GetStoredChecksumAsync(connection, fileName, stoppingToken);
if (string.Equals(existingChecksum, checksum, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("SP '{FileName}' is up-to-date (checksum match), skipping", fileName);
skipped++;
continue;
}
// ── اجرای SP (CREATE OR ALTER) ──
_logger.LogInformation("Deploying SP '{FileName}' (checksum changed: {Old} → {New})",
fileName,
existingChecksum ?? "NEW",
checksum[..12]);
try
{
await ExecuteSqlAsync(connection, sqlContent, stoppingToken);
await UpsertChecksumAsync(connection, fileName, checksum, stoppingToken);
deployed++;
_logger.LogInformation("Successfully deployed SP '{FileName}'", fileName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deploy SP '{FileName}' — skipping", fileName);
}
}
_logger.LogInformation(
"StoredProcedureDeploymentService completed — deployed: {Deployed}, skipped: {Skipped}, total: {Total}",
deployed, skipped, resourceNames.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "StoredProcedureDeploymentService encountered a fatal error");
}
}
// ══════════════════════════════════════════════════════════════
// Helper Methods
// ══════════════════════════════════════════════════════════════
///
/// ساخت جدول [CMS].[__StoredProcedureVersions] اگر وجود ندارد.
/// از Raw SQL استفاده میکنیم تا نیازی به Entity و Migration نباشد.
///
private static async Task EnsureVersionTableAsync(
System.Data.Common.DbConnection connection,
CancellationToken ct)
{
const string sql = """
IF NOT EXISTS (
SELECT 1 FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'CMS' AND TABLE_NAME = '__StoredProcedureVersions'
)
BEGIN
CREATE TABLE [CMS].[__StoredProcedureVersions] (
[FileName] NVARCHAR(256) NOT NULL PRIMARY KEY,
[Checksum] NVARCHAR(64) NOT NULL,
[DeployedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
[DeployCount] INT NOT NULL DEFAULT 1
);
END
""";
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync(ct);
}
///
/// خواندن checksum ذخیرهشده برای یک فایل
///
private static async Task GetStoredChecksumAsync(
System.Data.Common.DbConnection connection,
string fileName,
CancellationToken ct)
{
const string sql = "SELECT [Checksum] FROM [CMS].[__StoredProcedureVersions] WHERE [FileName] = @FileName";
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var param = cmd.CreateParameter();
param.ParameterName = "@FileName";
param.Value = fileName;
cmd.Parameters.Add(param);
var result = await cmd.ExecuteScalarAsync(ct);
return result as string;
}
///
/// ذخیره/بهروزرسانی checksum بعد از دیپلوی موفق
///
private static async Task UpsertChecksumAsync(
System.Data.Common.DbConnection connection,
string fileName,
string checksum,
CancellationToken ct)
{
const string sql = """
MERGE [CMS].[__StoredProcedureVersions] AS target
USING (SELECT @FileName AS [FileName]) AS source
ON target.[FileName] = source.[FileName]
WHEN MATCHED THEN
UPDATE SET [Checksum] = @Checksum,
[DeployedAt] = GETUTCDATE(),
[DeployCount] = target.[DeployCount] + 1
WHEN NOT MATCHED THEN
INSERT ([FileName], [Checksum], [DeployedAt], [DeployCount])
VALUES (@FileName, @Checksum, GETUTCDATE(), 1);
""";
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var pFileName = cmd.CreateParameter();
pFileName.ParameterName = "@FileName";
pFileName.Value = fileName;
cmd.Parameters.Add(pFileName);
var pChecksum = cmd.CreateParameter();
pChecksum.ParameterName = "@Checksum";
pChecksum.Value = checksum;
cmd.Parameters.Add(pChecksum);
await cmd.ExecuteNonQueryAsync(ct);
}
///
/// اجرای محتوای SQL روی کانکشن فعلی
///
private static async Task ExecuteSqlAsync(
System.Data.Common.DbConnection connection,
string sqlContent,
CancellationToken ct)
{
// SP ها ممکن است GO separator داشته باشند — هر بلاک را جدا اجرا میکنیم
var batches = SplitBatches(sqlContent);
foreach (var batch in batches)
{
var trimmed = batch.Trim();
if (string.IsNullOrWhiteSpace(trimmed)) continue;
await using var cmd = connection.CreateCommand();
cmd.CommandText = trimmed;
cmd.CommandTimeout = 120; // SP های بزرگ ممکن است زمان ببرند
await cmd.ExecuteNonQueryAsync(ct);
}
}
///
/// تقسیم SQL به بلاکها بر اساس GO separator
///
private static List SplitBatches(string sql)
{
// GO باید تنها در خط خودش باشد (case-insensitive)
var lines = sql.Split('\n');
var batch = new StringBuilder();
var result = new List();
foreach (var line in lines)
{
if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase))
{
if (batch.Length > 0)
{
result.Add(batch.ToString());
batch.Clear();
}
}
else
{
batch.AppendLine(line);
}
}
if (batch.Length > 0)
result.Add(batch.ToString());
return result;
}
///
/// خواندن محتوای یک Embedded Resource
///
private static async Task ReadResourceAsync(Assembly assembly, string resourceName)
{
await using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded resource not found: {resourceName}");
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync();
}
///
/// محاسبه SHA256 از محتوای SQL
///
private static string ComputeSha256(string content)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}