DD.WellWorkover.Cloud/AsbCloudInfrastructure/Repository/ChangeLogRepositoryAbstract.cs

436 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TEntity, TDto, TRequest> : IChangeLogRepository<TDto, TRequest>
where TDto : AsbCloudApp.Data.IId
where TEntity : ChangeLogAbstract
{
protected readonly IAsbCloudDbContext db;
public ChangeLogRepositoryAbstract(IAsbCloudDbContext db)
{
this.db = db;
}
private class ChangeLogQueryBuilder: IChangeLogQueryBuilder<TDto, TRequest>
{
protected readonly ChangeLogRepositoryAbstract<TEntity, TDto, TRequest> Repository;
protected IQueryable<TEntity> Query;
public ChangeLogQueryBuilder(
ChangeLogRepositoryAbstract<TEntity, TDto, TRequest> repository,
ChangeLogRequest request)
{
this.Repository = repository;
this.Query = repository.db.Set<TEntity>()
.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<TDto, TRequest> ApplyRequest(TRequest request)
{
this.Query = Repository.BuildQuery(request, Query);
return new ChangeLogQueryBuilderWithKnownTimezone(this, request);
}
public async Task<IEnumerable<TDto>> GetData(TimeSpan offset, CancellationToken token)
{
var dtos = await this.Query.Select(e => Repository.Convert(e, offset))
.ToArrayAsync(token);
return dtos;
}
public async Task<IEnumerable<ChangeLogDto<TDto>>> 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<TDto, TRequest>
{
TRequest request;
public ChangeLogQueryBuilderWithKnownTimezone(
ChangeLogQueryBuilder parentBuilder,
TRequest request)
:base(parentBuilder)
{
this.request = request;
}
public override IChangeLogQueryBuilderWithKnownTimezone<TDto, TRequest> ApplyRequest(TRequest request)
{
Query = Repository.BuildQuery(request, Query);
this.request = request;
return this;
}
public async Task<IEnumerable<TDto>> GetData(CancellationToken token)
{
TimeSpan timezoneOffset = Repository.GetTimezoneOffset(request);
var dtos = await this.GetData(timezoneOffset, token);
return dtos;
}
public async Task<IEnumerable<ChangeLogDto<TDto>>> GetChangeLogData(CancellationToken token)
{
TimeSpan timezoneOffset = Repository.GetTimezoneOffset(request);
var dtos = await this.GetChangeLogData(timezoneOffset, token);
return dtos;
}
}
public IChangeLogQueryBuilder<TDto, TRequest> GetQueryBuilder(ChangeLogRequest request)
{
var builder = new ChangeLogQueryBuilder(this, request);
return builder;
}
public async Task<int> InsertRange(int idUser, IEnumerable<TDto> 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<int> InsertRangeWithoutTransaction(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
var result = 0;
if (dtos.Any())
{
var entities = dtos.Select(Convert);
var creation = DateTimeOffset.UtcNow;
var dbSet = db.Set<TEntity>();
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<int> UpdateRange(int idUser, IEnumerable<TDto> 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<TEntity>();
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<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> 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<int> 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<int> ClearAndInsertRange(int idUser, TRequest request, IEnumerable<TDto> 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<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
var query = db.Set<TEntity>()
.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<IEnumerable<DateOnly>> 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<IEnumerable<ChangeLogDto<TDto>>> 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<IEnumerable<ChangeLogDto<TDto>>> CreateChangeLogDto(IQueryable<TEntity> 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<IEnumerable<TDto>> 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<TEntity> BuildQuery(TRequest request) =>
BuildQuery(request, db.Set<TEntity>()
.Include(e => e.Author)
.Include(e => e.Editor));
protected abstract IQueryable<TEntity> BuildQuery(TRequest request, IQueryable<TEntity> query);
protected virtual TEntity Convert(TDto dto)
{
var entity = dto.Adapt<TEntity>();
entity.Creation = entity.Creation.ToUniversalTime();
if (entity.Obsolete.HasValue)
entity.Obsolete = entity.Obsolete.Value.ToUniversalTime();
return entity;
}
protected virtual ChangeLogDto<TDto> ConvertChangeLogDto(TEntity entity, TimeSpan offset)
{
var changeLogDto = entity.Adapt<ChangeLogDto<TDto>>();
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<TDto>();
return dto;
}
private async Task<int> 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);
}
}