initial commit
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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<string, SemaphoreSlim> _locks = new();
|
||||
|
||||
public AutoNumberGeneratorService(AppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<string> 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}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user