using AsbCloudApp.Data; 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.IId where TEntity : ChangeLogAbstract { protected readonly IAsbCloudDbContext db; public ChangeLogRepositoryAbstract(IAsbCloudDbContext db) { this.db = db; } private class ChangeLogQueryBuilder: IChangeLogQueryBuilder { protected readonly ChangeLogRepositoryAbstract Repository; protected IQueryable Query; public ChangeLogQueryBuilder( ChangeLogRepositoryAbstract repository, ChangeLogRequest request) { this.Repository = repository; this.Query = repository.db.Set() .Include(e => e.Author) .Include(e => e.Editor); if (request.Moment.HasValue) { var momentUtc = request.Moment.Value.ToUniversalTime(); this.Query = this.Query .Where(e => e.Creation <= momentUtc) .Where(e => e.Obsolete == null || e.Obsolete >= momentUtc); } } public ChangeLogQueryBuilder(ChangeLogQueryBuilder builder) { Repository = builder.Repository; Query = builder.Query; } public virtual IChangeLogQueryBuilderWithKnownTimezone ApplyRequest(TRequest request) { this.Query = Repository.BuildQuery(request, Query); return new ChangeLogQueryBuilderWithKnownTimezone(this, request); } public async Task> GetData(TimeSpan offset, CancellationToken token) { var dtos = await this.Query.Select(e => Repository.Convert(e, offset)) .ToArrayAsync(token); return dtos; } public async Task>> GetChangeLogData(TimeSpan offset, CancellationToken token) { var dtos = await this.Query.Select(e => Repository.ConvertChangeLogDto(e, offset)) .ToArrayAsync(token); return dtos; } } private class ChangeLogQueryBuilderWithKnownTimezone: ChangeLogQueryBuilder, IChangeLogQueryBuilderWithKnownTimezone { TRequest request; public ChangeLogQueryBuilderWithKnownTimezone( ChangeLogQueryBuilder parentBuilder, TRequest request) :base(parentBuilder) { this.request = request; } public override IChangeLogQueryBuilderWithKnownTimezone ApplyRequest(TRequest request) { Query = Repository.BuildQuery(request, Query); this.request = request; return this; } public async Task> GetData(CancellationToken token) { TimeSpan timezoneOffset = Repository.GetTimezoneOffset(request); var dtos = await this.GetData(timezoneOffset, token); return dtos; } public async Task>> GetChangeLogData(CancellationToken token) { TimeSpan timezoneOffset = Repository.GetTimezoneOffset(request); var dtos = await this.GetChangeLogData(timezoneOffset, token); return dtos; } } public IChangeLogQueryBuilder GetQueryBuilder(ChangeLogRequest request) { var builder = new ChangeLogQueryBuilder(this, request); return builder; } public async Task InsertRange(int idUser, IEnumerable dtos, CancellationToken token) { using var transaction = db.Database.BeginTransaction(); try { var result = await InsertRangeWithoutTransaction(idUser, dtos, token); await transaction.CommitAsync(token); return result; } catch { await transaction.RollbackAsync(token); throw; } } private async Task InsertRangeWithoutTransaction(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(); try { 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); 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); try { result += await Clear(idUser, request, token); result += await InsertRangeWithoutTransaction(idUser, dtos, token); await transaction.CommitAsync(token); return result; } catch { await transaction.RollbackAsync(token); throw; } } public async Task MarkAsDeleted(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>> GetChangeLogForDate(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 dtos = await CreateChangeLogDto(query, offset, token); return dtos; } public async Task>> CreateChangeLogDto(IQueryable query, TimeSpan offset, CancellationToken token) { var entities = await query .OrderBy(e => e.Creation) .ThenBy(e => e.Obsolete) .ThenBy(e => e.Id) .ToListAsync(token); var dtos = entities.Select(e => ConvertChangeLogDto(e, offset)); return dtos; } public async Task> GetCurrent(TRequest request, CancellationToken token) { var changeLogRequest = new ChangeLogRequest() { Moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero) }; var builder = GetQueryBuilder(changeLogRequest) .ApplyRequest(request); var dtos = await builder.GetData(token); return dtos; } protected abstract TimeSpan GetTimezoneOffset(TRequest request); private IQueryable BuildQuery(TRequest request) => BuildQuery(request, db.Set() .Include(e => e.Author) .Include(e => e.Editor)); protected abstract IQueryable BuildQuery(TRequest request, IQueryable query); 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 ChangeLogDto ConvertChangeLogDto(TEntity entity, TimeSpan offset) { var changeLogDto = entity.Adapt>(); changeLogDto.Creation = entity.Creation.ToOffset(offset); if (entity.Obsolete.HasValue) changeLogDto.Obsolete = entity.Obsolete.Value.ToOffset(offset); changeLogDto.Item = Convert(entity, offset); return changeLogDto; } protected virtual TDto Convert(TEntity entity, TimeSpan offset) { var dto = entity.Adapt(); 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); } }