using Indotalent.Data.Entities; using Indotalent.Infrastructure.Database; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using System.Collections.Concurrent; using System.Data; namespace Indotalent.Infrastructure.AutoNumberGenerator; public class AutoNumberGeneratorService { private readonly AppDbContext _context; private static readonly ConcurrentDictionary _locks = new(); public AutoNumberGeneratorService(AppDbContext context) { _context = context; } public async Task GenerateNextNumberAsync( string tenantId, string entityName, string prefixTemplate, string? suffixTemplate = null, int paddingLength = 4, bool useYear = true, bool useMonth = false, CancellationToken cancellationToken = default) { var now = DateTime.UtcNow; int? year = useYear ? now.Year : null; int? month = useMonth ? now.Month : null; var lockKey = $"{tenantId}_{entityName}_{year}_{month}"; var semaphore = _locks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1)); await semaphore.WaitAsync(cancellationToken); try { var connection = _context.Database.GetDbConnection(); if (connection.State != ConnectionState.Open) { await connection.OpenAsync(cancellationToken); } var currentEfTransaction = _context.Database.CurrentTransaction?.GetDbTransaction(); long currentSequence = 0; var sql = @" MERGE INTO AutoNumberSequence AS Target USING (SELECT @TenantId AS TenantId, @EntityName AS EntityName, @Year AS Year, @Month AS Month) AS Source ON (Target.TenantId = Source.TenantId AND Target.EntityName = Source.EntityName AND (Target.Year = Source.Year OR (Target.Year IS NULL AND Source.Year IS NULL)) AND (Target.Month = Source.Month OR (Target.Month IS NULL AND Source.Month IS NULL))) WHEN MATCHED THEN UPDATE SET Target.CurrentSequence = Target.CurrentSequence + 1 WHEN NOT MATCHED THEN INSERT (Id, TenantId, EntityName, Year, Month, CurrentSequence, PrefixTemplate, SuffixTemplate, PaddingLength, IsDeleted) VALUES (@Id, Source.TenantId, Source.EntityName, Source.Year, Source.Month, 1, @PrefixTemplate, @SuffixTemplate, @PaddingLength, 0); SELECT CurrentSequence FROM AutoNumberSequence WHERE TenantId = @TenantId AND EntityName = @EntityName AND (Year = @Year OR (Year IS NULL AND @Year IS NULL)) AND (Month = @Month OR (Month IS NULL AND @Month IS NULL));"; using (var command = connection.CreateCommand()) { command.CommandText = sql; command.CommandType = CommandType.Text; if (currentEfTransaction != null) { command.Transaction = currentEfTransaction; } command.Parameters.Add(new SqlParameter("@Id", Guid.NewGuid().ToString("N").Substring(0, 30))); command.Parameters.Add(new SqlParameter("@TenantId", tenantId)); command.Parameters.Add(new SqlParameter("@EntityName", entityName)); command.Parameters.Add(new SqlParameter("@Year", (object?)year ?? DBNull.Value)); command.Parameters.Add(new SqlParameter("@Month", (object?)month ?? DBNull.Value)); command.Parameters.Add(new SqlParameter("@PrefixTemplate", prefixTemplate)); command.Parameters.Add(new SqlParameter("@SuffixTemplate", (object?)suffixTemplate ?? DBNull.Value)); command.Parameters.Add(new SqlParameter("@PaddingLength", paddingLength)); var result = await command.ExecuteScalarAsync(cancellationToken); currentSequence = Convert.ToInt64(result); } var formattedNumber = BuildFormattedNumber( entityName: entityName, prefixTemplate: prefixTemplate, suffixTemplate: suffixTemplate, paddingLength: paddingLength, sequence: currentSequence, now: now ); return formattedNumber; } finally { semaphore.Release(); if (semaphore.CurrentCount == 1) _locks.TryRemove(lockKey, out _); } } private string BuildFormattedNumber( string entityName, string prefixTemplate, string? suffixTemplate, int paddingLength, long sequence, DateTime now) { string ProcessTemplate(string? template) { if (string.IsNullOrEmpty(template)) return ""; return template .Replace("{EntityName}", entityName) .Replace("{Year}", now.Year.ToString()) .Replace("{Year2}", now.ToString("yy")) .Replace("{Month}", now.Month.ToString("D2")); } var prefix = ProcessTemplate(prefixTemplate); var suffix = ProcessTemplate(suffixTemplate); var paddedSequence = sequence.ToString($"D{paddingLength}"); return $"{prefix}{paddedSequence}{suffix}"; } }