using AsbCloudApp.Exceptions; using AsbCloudApp.Repositories; using AsbCloudApp.Requests; using AsbCloudDb.Model; using Mapster; using Microsoft.EntityFrameworkCore; using Npgsql; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Repository; public abstract class ChangeLogRepositoryAbstract : IChangeLogRepository where TDto : AsbCloudApp.Data.ChangeLogAbstract where TEntity : ChangeLogAbstract where TRequest : ChangeLogBaseRequest { protected readonly IAsbCloudDbContext db; public ChangeLogRepositoryAbstract(IAsbCloudDbContext db) { this.db = db; } public async Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token) { var result = 0; if (dtos.Any()) { var entities = dtos.Select(Convert); var creation = DateTimeOffset.UtcNow; var dbSet = db.Set(); foreach (var entity in entities) { entity.Id = default; entity.IdAuthor = idUser; entity.Creation = creation; entity.IdState = ChangeLogAbstract.IdStateActual; entity.IdEditor = null; entity.IdPrevious = null; entity.Obsolete = null; dbSet.Add(entity); } result += await SaveChangesWithExceptionHandling(token); } return result; } public async Task UpdateRange(int idUser, IEnumerable dtos, CancellationToken token) { if (dtos.Any(d => d.Id == 0)) throw new ArgumentInvalidException(nameof(dtos), "Отредактированные значения должны иметь id не 0"); if (!dtos.Any()) return 0; var updateTime = DateTimeOffset.UtcNow; var ids = dtos.Select(d => d.Id); var result = 0; var dbSet = db .Set(); var entitiesToDelete = await dbSet .Where(e => e.Obsolete == null) .Where(e => ids.Contains(e.Id)) .ToArrayAsync(token); var entitiesNotFound = dtos.Where(d => !entitiesToDelete.Any(e => e.Id == d.Id)); if (entitiesNotFound.Any()) { var notFoundIds = entitiesNotFound.Select(e => e.Id); var stringnotFoundIds = string.Join(", ", notFoundIds); throw new ArgumentInvalidException(nameof(dtos), $"записи с id:[{stringnotFoundIds}] не найдены, или не актуальны."); } using var transaction = db.Database.BeginTransaction(); foreach (var entity in entitiesToDelete) { entity.IdState = ChangeLogAbstract.IdStateReplaced; entity.Obsolete = updateTime; entity.IdEditor = idUser; } result += await db.SaveChangesAsync(token); var entitiesNew = dtos.Select(Convert); foreach (var entity in entitiesNew) { entity.IdPrevious = entity.Id; entity.Id = default; entity.Creation = updateTime; entity.IdAuthor = idUser; entity.Obsolete = null; entity.IdEditor = null; entity.IdState = ChangeLogAbstract.IdStateActual; dbSet.Add(entity); } result += await SaveChangesWithExceptionHandling(token); try { await transaction.CommitAsync(token); return result; } catch { await transaction.RollbackAsync(token); throw; } } public async Task UpdateOrInsertRange(int idUser, IEnumerable dtos, CancellationToken token) { var itemsToInsert = dtos.Where(e => e.Id == 0); var itemsToUpdate = dtos.Where(e => e.Id != 0); var result = 0; if (itemsToInsert.Any()) result += await InsertRange(idUser, itemsToInsert, token); if (itemsToUpdate.Any()) result += await UpdateRange(idUser, itemsToUpdate, token); return result; } public async Task Clear(int idUser, TRequest request, CancellationToken token) { var updateTime = DateTimeOffset.UtcNow; var query = BuildQuery(request); query = query.Where(e => e.Obsolete == null); var entitiesToDelete = await query.ToArrayAsync(token); foreach (var entity in entitiesToDelete) { entity.IdState = ChangeLogAbstract.IdCleared; entity.Obsolete = updateTime; entity.IdEditor = idUser; } var result = await SaveChangesWithExceptionHandling(token); return result; } public async Task ClearAndInsertRange(int idUser, TRequest request, IEnumerable dtos, CancellationToken token) { var result = 0; using var transaction = await db.Database.BeginTransactionAsync(token); result += await Clear(idUser, request, token); result += await InsertRange(idUser, dtos, token); try { await transaction.CommitAsync(token); return result; } catch { await transaction.RollbackAsync(token); throw; } } public async Task DeleteRange(int idUser, IEnumerable ids, CancellationToken token) { var updateTime = DateTimeOffset.UtcNow; var query = db.Set() .Where(e => ids.Contains(e.Id)) .Where(e => e.Obsolete == null); var entitiesToDelete = await query.ToArrayAsync(token); foreach (var entity in entitiesToDelete) { entity.IdState = ChangeLogAbstract.IdStateDeleted; entity.Obsolete = updateTime; entity.IdEditor = idUser; } var result = await SaveChangesWithExceptionHandling(token); return result; } public async Task> GetDatesChange(TRequest request, CancellationToken token) { var query = BuildQuery(request); var datesCreateQuery = query .Select(e => e.Creation) .Distinct(); var datesCreate = await datesCreateQuery.ToArrayAsync(token); var datesUpdateQuery = query .Where(e => e.Obsolete != null) .Select(e => e.Obsolete!.Value) .Distinct(); var datesUpdate = await datesUpdateQuery.ToArrayAsync(token); TimeSpan offset = GetTimezoneOffset(request); var dates = Enumerable.Concat(datesCreate, datesUpdate); dates = dates.Select(date => date.ToOffset(offset)); var datesOnly = dates .Select(d => new DateOnly(d.Year, d.Month, d.Day)) .Distinct() .OrderBy(d => d); return datesOnly; } public async Task> GetChangeLog(TRequest request, DateOnly? date, CancellationToken token) { var query = BuildQuery(request); TimeSpan offset = GetTimezoneOffset(request); if (date.HasValue) { var min = new DateTimeOffset(date.Value.Year, date.Value.Month, date.Value.Day, 0, 0, 0, offset).ToUniversalTime(); var max = min.AddDays(1); var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max); var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max); query = createdQuery.Union(editedQuery); } var entities = await query .OrderBy(e => e.Creation) .ThenBy(e => e.Obsolete) .ThenBy(e => e.Id) .ToListAsync(token); var dtos = entities.Select(e => Convert(e, offset)); return dtos; } public async Task> Get(TRequest request, CancellationToken token) { var query = BuildQuery(request); var entities = await query .OrderBy(e => e.Creation) .ThenBy(e => e.Obsolete) .ThenBy(e => e.Id) .ToArrayAsync(token); TimeSpan offset = GetTimezoneOffset(request); var dtos = entities.Select(e => Convert(e, offset)); return dtos; } protected abstract TimeSpan GetTimezoneOffset(TRequest request); protected abstract IQueryable BuildQuery(TRequest request); protected virtual TEntity Convert(TDto dto) { var entity = dto.Adapt(); entity.Creation = entity.Creation.ToUniversalTime(); if(entity.Obsolete.HasValue) entity.Obsolete = entity.Obsolete.Value.ToUniversalTime(); return entity; } protected virtual TDto Convert(TEntity entity, TimeSpan offset) { var dto = entity.Adapt(); dto.Creation = entity.Creation.ToOffset(offset); if (entity.Obsolete.HasValue) dto.Obsolete = entity.Obsolete.Value.ToOffset(offset); return dto; } private async Task SaveChangesWithExceptionHandling(CancellationToken token) { try { var result = await db.SaveChangesAsync(token); return result; } catch (DbUpdateException ex) { if (ex.InnerException is PostgresException pgException) TryConvertPostgresExceptionToValidateException(pgException); throw; } } private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException) { if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation) throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail); } }