Добавить таблицу для учета комментариев и действий пользователя для вывода статистики по ChangeLog #30

Open
on.nemtina wants to merge 24 commits from feature/#956-change-log-table-comment into master
25 changed files with 1117 additions and 396 deletions

View File

@ -1,107 +1,124 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using DD.Persistence.API;
using DD.Persistence.API.Services;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using System.Net;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using UuidExtensions;
namespace DD.Persistence.API.Controllers;
/// <summary>
/// Контроллер по работе с журналом изменений
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]

Не ошибка, но почему не private readonly поле как везде? +Свойства == синтаксический сахар для методов Get_..() Set_..(), А значит они должны называться с большой буквы.

Не ошибка, но почему не private readonly поле как везде? +Свойства == синтаксический сахар для методов Get_..() Set_..(), А значит они должны называться с большой буквы.
public class ChangeLogController : ControllerBase, IChangeLogApi
{
private readonly IChangeLogRepository repository;
private ChangeLogService service { get; }
public ChangeLogController(IChangeLogRepository repository)
/// <summary>
/// ctor
/// </summary>
/// <param name="service"></param>
public ChangeLogController(ChangeLogService service)

Должен остаться только один

Должен остаться только один
{
this.repository = repository;
this.service = service;

Было бы здорова дать методам контроллеров текстовое описание для сваггера.

Было бы здорова дать методам контроллеров текстовое описание для сваггера.
}
/// <summary>
/// Добавить записи в журнал изменений по дискриминатору
/// </summary>
/// <param name="idDiscriminator"></param>

Скорее всего этот метод лишний.

Скорее всего этот метод лишний.
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator,
[FromBody] ChangeLogValuesDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, [dto], token);
return CreatedAtAction(nameof(Add), result);
}
[HttpPost("range/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token);
var changeLogCommitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var result = await service.AddRange(idDiscriminator, changeLogCommitRequest, dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
/// <summary>
/// Удалить записи в журнале изменений
/// </summary>

Скорее всего в ChangeLogCommitDto dtos - лишнее, и помешаются в методе delete например.
Отношение строчек с данными к коммиту делается уже внутри сервиса.

Скорее всего в ChangeLogCommitDto dtos - лишнее, и помешаются в методе delete например. Отношение строчек с данными к коммиту делается уже внутри сервиса.
/// <param name="ids"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(Guid id, CancellationToken token)
public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, [id], token);
var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.MarkAsDeleted(ids, changeLogCommitRequest, token);
return Ok(result);
}
[HttpDelete("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, ids, token);
return Ok(result);
}
/// <summary>
/// Очистить все записи в журнале изменений (по дискриминатору) и добавить новые
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("replace/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token);
var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.ClearAndAddRange(idDiscriminator, changeLogCommitRequest, dtos, token);
return Ok(result);
}
/// <summary>
/// сохранить изменения в записях журнала изменений
/// </summary>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Update(
ChangeLogValuesDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, [dto], token);
return Ok(result);
}
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange(
IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token);
var changeLogCommitRequest = new CreateChangeLogCommitRequest(userId, comment);
var result = await service.UpdateRange(changeLogCommitRequest, dtos, token);
return Ok(result);
}
/// <summary>
/// Получение актуальных записей (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent(
@ -110,11 +127,19 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
CancellationToken token)
{
var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token);
var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result);
}
/// <summary>
/// Получение записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate(
@ -123,11 +148,19 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token);
var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result);
}
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
@ -137,37 +170,57 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
DateTimeOffset dateEnd,
CancellationToken token)
{
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
var result = await service.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return Ok(result);
}
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesChange/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesChange(idDiscriminator, token);
var result = await service.GetDatesChange(idDiscriminator, token);
return Ok(result);
}
/// <summary>
/// Получение данных, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<ChangeLogValuesDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
var result = await service.GetGtDate(idDiscriminator, dateBegin, token);
return Ok(result);
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesRange(idDiscriminator, token);
var result = await service.GetDatesRange(idDiscriminator, token);
if (result is null)
return NoContent();
@ -232,7 +285,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
IdEditor = userId,
IdNext = changeLogItemCurrentId,
Obsolete = DateTimeOffset.UtcNow.AddDays(-5),
Value = new ChangeLogValuesDto(){
Value = new ChangeLogValuesDto(){
Id = Guid.CreateVersion7(),
Value = new Dictionary<string, object>() {
["1"] = new { id = 1, caption = "Изменение 1" },
@ -242,7 +295,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
}
};
var result = new List<HistoryChangeLogDto>() {
new() {
new() {
Comment = "Петров И. Ю. попросил внести изменения",
DateTime = changeLogItemCreation,
DiscriminatorId = Guid.CreateVersion7(),

View File

@ -9,6 +9,8 @@ using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
using DD.Persistence.Database.Entity;
using DD.Persistence.API.Services;
namespace DD.Persistence.API;
@ -53,6 +55,7 @@ public static class DependencyInjection
{
services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
services.AddTransient<ChangeLogService>();
}
#region Authentication

View File

@ -0,0 +1,198 @@
using DD.Persistence.Models.Common;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Entity;
namespace DD.Persistence.API.Services;
/// <summary>
/// Сервис по работе с журналом изменений
/// </summary>
public class ChangeLogService
{
private readonly IMemoryCache memoryCache;
private readonly IChangeLogCommitRepository commitRepository;
private readonly IChangeLogRepository repository;
private readonly TimeSpan? AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60);
/// <summary>
/// ctor
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="commitRepository"></param>
/// <param name="repository"></param>
public ChangeLogService(
IMemoryCache memoryCache,
IChangeLogCommitRepository commitRepository,
IChangeLogRepository repository)
{
this.memoryCache = memoryCache;
this.commitRepository = commitRepository;
this.repository = repository;
}
/// <summary>
/// Чтение ключа коммита из кеша или (если коммита в кеше нет) создание коммита

Тут мы не читаем данные коммита. Только получаем его Id.
Не правильное название и описание.

Тут мы не читаем данные коммита. Только получаем его Id. Не правильное название и описание.
/// </summary>
/// <param name="commitDto"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<Guid> GetOrCreateCommitAsync(CreateChangeLogCommitRequest commitDto, CancellationToken token)
Review

Давай в кеше хранить комит целиком не только ID

Давай в кеше хранить комит целиком не только ID
{
var key = (commitDto.IdAuthor, commitDto.Comment);
var commitId = await memoryCache.GetOrCreateAsync(key, async (cacheEntry) =>
{
cacheEntry.AbsoluteExpirationRelativeToNow = AbsoluteExpirationRelativeToNow;
var commitId = await commitRepository.Add(commitDto, token);
return commitId;
});
return commitId;
}
/// <summary>
/// Добавление записи в журнал изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> AddRange(Guid idDiscriminator, CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.AddRange(idDiscriminator, commitDto, dtos, token);
return result;
}
/// <summary>
/// Пометить запись журнала изменений как удаленную
/// </summary>
/// <param name="ids"></param>
/// <param name="commitRequestDto"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> MarkAsDeleted(IEnumerable<Guid> ids, CreateChangeLogCommitRequest commitRequestDto, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.MarkAsDeleted(commitId, ids, commitDto.Creation, token);
return result;
}
/// <summary>
/// Очистить старые и добавить новые записи в журнал изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.ClearAndAddRange(idDiscriminator, commitDto, dtos, token);
return result;
}
/// <summary>
/// Обновить записи в журнале изменений
/// </summary>
/// <param name="commitRequestDto"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<int> UpdateRange(CreateChangeLogCommitRequest commitRequestDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var commitId = await GetOrCreateCommitAsync(commitRequestDto, token);
var commitDto = new ChangeLogCommitDto(commitId, commitRequestDto);
var result = await repository.UpdateRange(commitDto, dtos, token);
return result;
}
/// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="momentUtc"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, momentUtc, paginationRequest, token);
return result;
}
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return result;
}
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesChange(idDiscriminator, token);
return result;
}
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
return result;
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesRange(idDiscriminator, token);
return result;
}
}

View File

@ -19,10 +19,10 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
}
/// <inheritdoc/>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, token), token);
async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, comment, token), token);
return result;
}
@ -47,55 +47,28 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
}
/// <inheritdoc/>
public async Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token)
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Add(idDiscriminator, dto, token), token);
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, comment, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, token), token);
async () => await refitChangeLogClient.UpdateRange(dtos, comment, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> Update(ChangeLogValuesDto dto, CancellationToken token)
public async Task<int> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Update(dto, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.UpdateRange(dtos, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> Delete(Guid id, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Delete(id, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.DeleteRange(ids, token), token);
async () => await refitChangeLogClient.DeleteRange(ids, comment, token), token);
return result;
}

View File

@ -9,48 +9,34 @@ namespace DD.Persistence.Client.Clients.Interfaces;
/// </summary>
public interface IChangeLogClient : IDisposable
{
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
@ -80,19 +66,12 @@ public interface IChangeLogClient : IDisposable
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Update(ChangeLogValuesDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
}

View File

@ -16,7 +16,7 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
@ -34,41 +34,23 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
[Get($"{BaseRoute}/history/{{idDiscriminator}}")]
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(ChangeLogValuesDto dto, CancellationToken token);
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
[Put($"{BaseRoute}/range")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id, CancellationToken token);
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
[Delete($"{BaseRoute}/range")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, CancellationToken token);
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)

View File

@ -13,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistencePostgresContext))]
[Migration("20250210055116_Init")]
[Migration("20250221053248_Init")]
partial class Init
{
/// <inheritdoc />
@ -41,18 +41,18 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
.HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
@ -64,9 +64,38 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id коммита");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{
b.Property<Guid>("SystemId")
@ -214,6 +243,23 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -224,6 +270,13 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Navigation("ChangeLogCreatedItems");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618
}
}

View File

@ -13,21 +13,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "change_log",
name: "change_log_commit",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Автор изменения"),
IdEditor = table.Column<Guid>(type: "uuid", nullable: true, comment: "Редактор"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение")
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита"),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Пользователь, создавший коммит"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания коммита"),
Comment = table.Column<string>(type: "text", nullable: false, comment: "Комментарий к коммиту")
},
constraints: table =>
{
table.PrimaryKey("PK_change_log", x => x.Id);
table.PrimaryKey("PK_change_log_commit", x => x.Id);
});
migrationBuilder.CreateTable(
@ -98,6 +94,35 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp });
});
migrationBuilder.CreateTable(
name: "change_log",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение"),
IdCreatedCommit = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита на создание записи"),
IdObsoletedCommit = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id коммита на устаревание записи")
},
constraints: table =>
{
table.PrimaryKey("PK_change_log", x => x.Id);
table.ForeignKey(
name: "FK_change_log_change_log_commit_IdCreatedCommit",
column: x => x.IdCreatedCommit,
principalTable: "change_log_commit",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_change_log_change_log_commit_IdObsoletedCommit",
column: x => x.IdObsoletedCommit,
principalTable: "change_log_commit",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "tech_message",
columns: table => new
@ -120,6 +145,16 @@ namespace DD.Persistence.Database.Postgres.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_change_log_IdCreatedCommit",
table: "change_log",
column: "IdCreatedCommit");
migrationBuilder.CreateIndex(
name: "IX_change_log_IdObsoletedCommit",
table: "change_log",
column: "IdObsoletedCommit");
migrationBuilder.CreateIndex(
name: "IX_tech_message_SystemId",
table: "tech_message",
@ -147,6 +182,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "timestamped_values");
migrationBuilder.DropTable(
name: "change_log_commit");
migrationBuilder.DropTable(
name: "data_source_system");
}

View File

@ -38,18 +38,18 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
.HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
@ -61,9 +61,38 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id коммита");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
{
b.Property<Guid>("SystemId")
@ -211,6 +240,23 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -221,6 +267,13 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Navigation("ChangeLogCreatedItems");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618
}
}

View File

@ -45,6 +45,7 @@ public static class DependencyInjection
//services.AddTransient(typeof(PersistenceRepository<TimestampedValues>));
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogCommitRepository, ChangeLogCommitRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();

View File

@ -19,12 +19,6 @@ public class ChangeLog : IDiscriminatorItem, IChangeLog
[Comment("Дискриминатор таблицы")]
public Guid DiscriminatorId { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
[Comment("Редактор")]
public Guid? IdEditor { get; set; }
[Comment("Дата создания записи")]
public DateTimeOffset Creation { get; set; }
Review

Комментарий про денормализацию БД

Комментарий про денормализацию БД
@ -36,4 +30,16 @@ public class ChangeLog : IDiscriminatorItem, IChangeLog
[Column(TypeName = "jsonb"), Comment("Значение")]
public required IDictionary<string, object> Value { get; set; }
[Required, Comment("Id коммита на создание записи")]
public Guid IdCreatedCommit { get; set; }
[Comment("Id коммита на устаревание записи")]
public Guid? IdObsoletedCommit { get; set; }

Для устаревших записей у нас есть 2 коммита: один при создании записи и еще один при устаревании.
Кстати туда же можно унести инфо о пользователях и датах.

Для устаревших записей у нас есть 2 коммита: один при создании записи и еще один при устаревании. Кстати туда же можно унести инфо о пользователях и датах.
[Required, ForeignKey(nameof(IdCreatedCommit)), Comment("Коммит пользователя на создание записи")]
public virtual ChangeLogCommit CreatedCommit { get; set; } = null!;
[ForeignKey(nameof(IdObsoletedCommit)), Comment("Коммит пользователя на устаревание записи")]
public virtual ChangeLogCommit? ObsoletedCommit { get; set; }
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Database.Entity;
/// <summary>
/// Таблица c коммитами пользователей
/// </summary>
[Table("change_log_commit")]
public class ChangeLogCommit
{
[Key, Comment("Id коммита")]
public Guid Id { get; set; }
[Comment("Пользователь, создавший коммит")]
public Guid IdAuthor { get; set; }
[Comment("Дата создания коммита")]
public DateTimeOffset Creation { get; set; }
[Comment("Комментарий к коммиту")]
public required string Comment { get; set; }
[Required, InverseProperty(nameof(ChangeLog.CreatedCommit)), Comment("Записи, добавленные в журнал изменений")]
public virtual ICollection<ChangeLog> ChangeLogCreatedItems { get; set; } = null!;
[InverseProperty(nameof(ChangeLog.ObsoletedCommit)), Comment("Устаревшие записи в журнале изменений")]
public virtual ICollection<ChangeLog>? ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -10,16 +10,6 @@ public interface IChangeLog
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public Guid? IdEditor { get; set; }

Свойства не должны быть реализованы в интерфейсе.
А почему автор коммита устаревания удален, а автор коммита создания нет?

Свойства не должны быть реализованы в интерфейсе. А почему автор коммита устаревания удален, а автор коммита создания нет?
/// <summary>
/// Дата создания записи
/// </summary>

View File

@ -16,6 +16,8 @@ public class PersistenceDbContext : DbContext
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<ChangeLogCommit> ChangeLogCommit => Set<ChangeLogCommit>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>();

View File

@ -0,0 +1,39 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogCommitRepository : IChangeLogCommitRepository
{
private DbContext db;
public ChangeLogCommitRepository(DbContext db)
{
this.db = db;
}
public async Task<Guid> Add(CreateChangeLogCommitRequest commitRequestDto, CancellationToken token)
Review

commitRequestDto -> commitRequest или request

commitRequestDto -> commitRequest или request
{
var commit = new ChangeLogCommit()
{
Id = Uuid7.Guid(),
IdAuthor = commitRequestDto.IdAuthor,
Comment = commitRequestDto.Comment,
Creation = commitRequestDto.Creation,
};
db.Add(commit);
await db.SaveChangesAsync();
return commit.Id;
}
}

View File

@ -6,6 +6,7 @@ using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
@ -18,12 +19,20 @@ public class ChangeLogRepository : IChangeLogRepository
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> AddRange(Guid idDiscriminator, ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
foreach (var values in dtos)
{
var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto);
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = commitDto.Creation,
DiscriminatorId = idDiscriminator,
Value = values.Value,
IdCreatedCommit = commitDto.Id,
};
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
@ -33,7 +42,7 @@ public class ChangeLogRepository : IChangeLogRepository
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token)
public async Task<int> MarkAsDeleted(Guid idCommit, IEnumerable<Guid> ids, DateTimeOffset updateTime, CancellationToken token)
Review

Как будто и idCommit и updateTime есть в ChangeLogCommitDto

Как будто и idCommit и updateTime есть в ChangeLogCommitDto
{
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
@ -46,12 +55,16 @@ public class ChangeLogRepository : IChangeLogRepository
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
foreach (var entity in entities)
{
entity.Obsolete = updateTime;
entity.IdObsoletedCommit = idCommit;
}
return result;
return await db.SaveChangesAsync(token);
}
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
public async Task<int> MarkAsDeleted(Guid idDiscriminator, Guid idCommit, DateTimeOffset updateTime, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.DiscriminatorId == idDiscriminator)
@ -59,40 +72,34 @@ public class ChangeLogRepository : IChangeLogRepository
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
private async Task<int> MarkAsObsolete(Guid idEditor, IEnumerable<ChangeLog> entities, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
foreach (var entity in entities)
{
entity.Obsolete = updateTime;
entity.IdEditor = idEditor;
entity.DiscriminatorId = idCommit;
}

Это не правда

Это не правда
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> ClearAndAddRange(Guid idDiscriminator, ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{

Этот метод должен помечать все записи относящиеся к дискриминатору как удаленные и добавлять новые.

Этот метод должен помечать все записи относящиеся к дискриминатору как удаленные и добавлять новые.
var result = 0;
var changeLogIds = dtos.Select(c => c.Id);
var comment = commitDto.Comment;
Review

эта логкальная переменная дальше нигде не используется. Зачем она здесь?

эта логкальная переменная дальше нигде не используется. Зачем она здесь?
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeleted(idAuthor, idDiscriminator, token);
result += await AddRange(idAuthor, idDiscriminator, dtos, token);
result += await MarkAsDeleted(commitDto.Id, changeLogIds, commitDto.Creation, token);
Review

Это не то.
Покрой этот метод интеграционным тестом плиз.

// arrange
add some data winth 2 discriminators
// act replace data for 1 discriminaqtor
// assert
check thar othe data with othe discr

Это не то. Покрой этот метод интеграционным тестом плиз. // arrange add some data winth 2 discriminators // act replace data for 1 discriminaqtor // assert check thar othe data with othe discr
result += await AddRange(idDiscriminator, commitDto, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
public async Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
@ -101,7 +108,6 @@ public class ChangeLogRepository : IChangeLogRepository
.Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id);

Linq очень не оптимально материализует в словари.
Тут лучше материализовать сперва в массив, а затем массив в словарь.

  • Используй асинхронные методы материализации в асинхронных методах репозиториев
Linq очень не оптимально материализует в словари. Тут лучше материализовать сперва в массив, а затем массив в словарь. + Используй асинхронные методы материализации в асинхронных методах репозиториев
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
foreach (var dto in dtos)
@ -112,20 +118,25 @@ public class ChangeLogRepository : IChangeLogRepository
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.DiscriminatorId, dto);
var newEntity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = commitDto.Creation,
DiscriminatorId = updatedEntity.DiscriminatorId,
Value = dto.Value,
IdCreatedCommit = commitDto.Id,
};
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.Obsolete = DateTimeOffset.UtcNow;
updatedEntity.IdEditor = idEditor;
updatedEntity.Obsolete = commitDto.Creation;
updatedEntity.IdObsoletedCommit = commitDto.Id;
}
result = await db.SaveChangesAsync(token);
var result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
@ -195,22 +206,6 @@ public class ChangeLogRepository : IChangeLogRepository
return datesOnly;
}
private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, ChangeLogValuesDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
DiscriminatorId = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
};
return entity;
}
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();

View File

@ -15,6 +15,7 @@ namespace DD.Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest
{
private readonly IChangeLogClient client;
private readonly PaginationRequest paginationRequest;
private static readonly Random generatorRandomDigits = new();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
@ -25,22 +26,13 @@ public class ChangeLogControllerTest : BaseIntegrationTest
client = scope.ServiceProvider
.GetRequiredService<IChangeLogClient>();
}
[Fact]
public async Task ClearAndInsertRange_InEmptyDb()
{
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
// assert
Assert.Equal(2, result);
paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
}
[Fact]
@ -48,33 +40,17 @@ public class ChangeLogControllerTest : BaseIntegrationTest
{
// arrange
var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var idDiscriminator = createdResult.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<ChangeLogValuesDto>());
var newEntitiesData = await CreateAndReturnNewDtos(insertedCount, (-15, -1));
var idDiscriminator = newEntitiesData.Item1;
var dtos = newEntitiesData.Item2;
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
//act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, "Добавление новых элементов и очистка старых", CancellationToken.None);
// assert
Assert.Equal(insertedCount * 2, result);
}
[Fact]
public async Task Add_returns_success()
{
// arrange
var count = 1;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var dto = dtos.FirstOrDefault()!;
// act
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
// assert
Assert.Equal(count, result);
}
[Fact]
public async Task AddRange_returns_success()
{
@ -82,9 +58,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var count = 3;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var comment = "Создаю 3 элемента";
// act
var result = await client.AddRange(idDiscriminator, dtos, new CancellationToken());
var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None);
// assert
Assert.Equal(count, result);
@ -93,13 +70,14 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task Update_returns_success()
{
// arrange
//arrange
dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
var comment = "Создаю 1 элемент";
var result = await client.AddRange(idDiscriminator, [dto], comment, CancellationToken.None);
var entity = dbContext.ChangeLog
.Where(x => x.DiscriminatorId == idDiscriminator)
@ -107,7 +85,8 @@ public class ChangeLogControllerTest : BaseIntegrationTest
dto = entity.Adapt<ChangeLogValuesDto>();
// act
result = await client.Update(dto, new CancellationToken());
comment = "Обновляю 1 элемент";
result = await client.UpdateRange([dto], comment, CancellationToken.None);
// assert
Assert.Equal(2, result);
@ -139,71 +118,49 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task UpdateRange_returns_success()
{
// arrange
var count = 2;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new ChangeLogValuesDto()
{
Id = c.Id,
Value = c.Value
}).ToArray();
var comment = "Создаю 3 элемента";
// act
var result = await client.UpdateRange(dtos, new CancellationToken());
var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None);
var paginatedResult = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddDays(1), paginationRequest, CancellationToken.None);
// act
comment = "Обновляю 3 элемента";
result = await client.UpdateRange(paginatedResult.Items, comment, CancellationToken.None);
// assert
Assert.Equal(count * 2, result);
}
[Fact]
public async Task Delete_returns_success()
{
// arrange
var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!;
var entity = dto.Adapt<ChangeLog>();
dbContext.ChangeLog.Add(entity);
dbContext.SaveChanges();
// act
var result = await client.Delete(entity.Id, new CancellationToken());
// assert
Assert.Equal(1, result);
}
[Fact]
public async Task DeleteRange_returns_success()
{
// arrange
var count = 10;
var dtos = Generate(count);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
var insertedCount = 10;
var newEntitiesData = await CreateAndReturnNewDtos(insertedCount, (-15, -1));
var idDiscriminator = newEntitiesData.Item1;
var dtos = newEntitiesData.Item2;
// act
var ids = entities.Select(e => e.Id);
var result = await client.DeleteRange(ids, new CancellationToken());
var ids = dtos.Select(e => e.Id);
var result = await client.DeleteRange(ids, "Удаление нескольких записей", CancellationToken.None);
// assert
Assert.Equal(count, result);
Assert.Equal(insertedCount, result);
}
[Fact]
public async Task GetDatesRange_returns_success()
{
// arrange
var changeLogItems = CreateChangeLogItems(3, (-15, 15));
//arrange
var changeLogItems = await CreateAndReturnNewEntities(3, (-15, -1));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2.OrderBy(e => e.Creation);
var entities = changeLogItems.Item2.OrderBy(c => c.Creation);
// act
var result = await client.GetDatesRange(idDiscriminator, new CancellationToken());
var result = await client.GetDatesRange(idDiscriminator, CancellationToken.None);
// assert
Assert.NotNull(result);
@ -228,7 +185,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
//создаем записи
var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var changeLogItems = await CreateAndReturnNewDtos(count, (-15, -1));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
@ -237,14 +194,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var ids = entities.Select(e => e.Id);
var idsToDelete = ids.Skip(2);
var deletedCount = await client.DeleteRange(idsToDelete, new CancellationToken());
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
var deletedCount = await client.DeleteRange(idsToDelete, "Удаление нескольких записей", CancellationToken.None);
var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken());
@ -260,8 +210,8 @@ public class ChangeLogControllerTest : BaseIntegrationTest
}
[Theory]
[InlineData(5, -15, 15, -20, 20, 10)]
[InlineData(5, -15, -10, -16, -9, 5)]
[InlineData(5, -15, -5, -20, 20, 10)]
[InlineData(5, -15, -10, -16, 9, 10)]
public async Task GetChangeLogForInterval_returns_success(
int insertedCount,
int daysBeforeNowChangeLog,
@ -276,17 +226,16 @@ public class ChangeLogControllerTest : BaseIntegrationTest
//создаем записи
var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange);
var changeLogItems = await CreateAndReturnNewDtos(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
var dtos = changeLogItems.Item2;
var dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>()).ToArray();
await client.UpdateRange(dtos, new CancellationToken());
await client.UpdateRange(dtos, "Обновляем несколько записей", CancellationToken.None);
//act
var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter);
var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter);
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, new CancellationToken());
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, CancellationToken.None);
//assert
Assert.NotNull(result);
@ -308,7 +257,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
}
private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange)
private async Task<(Guid, IEnumerable<ChangeLogValuesDto>)> CreateAndReturnNewDtos(int count, (int, int) daysRange)
{
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
@ -323,8 +272,43 @@ public class ChangeLogControllerTest : BaseIntegrationTest
return entity;
}).ToArray();
dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>());
// act
var result = await client.AddRange(idDiscriminator, dtos, "Добавление элементов", CancellationToken.None);
var paginatedResult = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddDays(1), paginationRequest, CancellationToken.None);
return (idDiscriminator, paginatedResult.Items);
}
private async Task<(Guid, IEnumerable<ChangeLog>)> CreateAndReturnNewEntities(int count, (int, int) daysRange)
{
var commit = new ChangeLogCommit()
{
Comment = "Комментарий к коммиту",
Creation = DateTimeOffset.UtcNow,
Id = Guid.NewGuid(),
};
dbContext.ChangeLogCommit.Add(commit);
await dbContext.SaveChangesAsync();
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
Guid idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.DiscriminatorId = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
entity.IdCreatedCommit = commit.Id;
return entity;
}).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
await dbContext.SaveChangesAsync();
return (idDiscriminator, entities);
}

View File

@ -0,0 +1,21 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Модель коммита с изменениями
/// </summary>
public class ChangeLogCommitDto : CreateChangeLogCommitRequest
{
/// <summary>
/// Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
///
/// </summary>
public ChangeLogCommitDto(Guid id, CreateChangeLogCommitRequest request) : base(request.IdAuthor, request.Comment)
{
Id = id;
}
}

View File

@ -0,0 +1,33 @@
namespace DD.Persistence.Models.Requests;
/// <summary>
/// Модель для создания коммита
/// </summary>
public class CreateChangeLogCommitRequest
{
/// <summary>
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Пользователь, совершающий коммит
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Комментарий
/// </summary>
public string Comment { get; set; } = string.Empty;
/// <summary>
///
/// </summary>
public CreateChangeLogCommitRequest(Guid idAuthor, string comment)
{
IdAuthor = idAuthor;
Comment = comment;
Creation = DateTimeOffset.UtcNow;
}
}

View File

@ -11,8 +11,8 @@
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="Testcontainers" Version="4.1.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.1.0" />
<PackageReference Include="Testcontainers" Version="4.2.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.2.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@ -0,0 +1,279 @@
using DD.Persistence.API.Services;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Microsoft.Extensions.Caching.Memory;
using NSubstitute;
using UuidExtensions;
namespace DD.Persistence.Test;
public class ChangeLogTest
{
private readonly IChangeLogCommitRepository changeLogCommitRepository = Substitute.For<IChangeLogCommitRepository>();
private readonly IChangeLogRepository changeLogRepository = Substitute.For<IChangeLogRepository>();
private ChangeLogService service;
public ChangeLogTest()
{
var memoryCache = new MemoryCache(new MemoryCacheOptions());
service = new ChangeLogService(memoryCache, changeLogCommitRepository, changeLogRepository);
}
[Fact]
public async Task AddRange()
{
//arrange

Расставь пож. во всех тестах комментарии "//act" и "//assert" чтобы было понятнее где подготовительные операции, где проверяемое действие и где начинаются проверки

Расставь пож. во всех тестах комментарии "//act" и "//assert" чтобы было понятнее где подготовительные операции, где проверяемое действие и где начинаются проверки
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Добавление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(Uuid7.Guid());
changeLogRepository
.AddRange(
Arg.Any<Guid>(),
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var addRangeResult = await service
.AddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
addRangeResult = await service
.AddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).AddRange(discriminatorId, Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task UpdateRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Изменение нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(commit.Id);
changeLogRepository
.UpdateRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(3).UpdateRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task MarkAsDeleted()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Удаление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(Uuid7.Guid(), comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(expectedCommitId);
changeLogRepository
.MarkAsDeleted(
Arg.Any<Guid>(),
Arg.Any<IEnumerable<Guid>>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).MarkAsDeleted(commit.Id, dtoIds, Arg.Any<DateTimeOffset>(), CancellationToken.None);
}
[Fact]
public async Task ClearAndAddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var expectedCommitId = Uuid7.Guid();
var comment = "Удаление и добавление нескольких значений";
var commitRequest = new CreateChangeLogCommitRequest(expectedCommitId, comment);
var commit = new ChangeLogCommitDto(expectedCommitId, commitRequest);
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<CreateChangeLogCommitRequest>(), Arg.Any<CancellationToken>()).Returns(Uuid7.Guid());
changeLogRepository
.ClearAndAddRange(
Arg.Any<Guid>(),
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogValuesDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var clearAndAddResult = await service
.ClearAndAddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
clearAndAddResult = await service
.ClearAndAddRange(discriminatorId, commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).ClearAndAddRange(discriminatorId, Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task GetByDate()
{
//arrange
var discriminatorId = Uuid7.Guid();
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 1000
};
var dtos = GenerateChangeLogValuesDto(5);
var items = new PaginationContainer<ChangeLogValuesDto>()
{
Take = paginationRequest.Take,
Skip = paginationRequest.Skip,
Items = dtos,
Count = 10
};
var momentDate = DateTime.UtcNow;
changeLogRepository
.GetByDate(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<PaginationRequest>(),
Arg.Any<CancellationToken>())
.Returns(items);
//act
var actualItems = await service
.GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
}
[Fact]
public async Task GetChangeLogForInterval()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dtos = GenerateChangeLogDto(5);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
changeLogRepository
.GetChangeLogForInterval(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
}
[Fact]
public async Task GetDatesChange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
var dateOnlyBegin = new DateOnly(dateBegin.Year, dateBegin.Month, dateBegin.Day);
var dateOnlyEnd = new DateOnly(dateEnd.Year, dateEnd.Month, dateEnd.Day);
var dtos = new List<DateOnly>() { dateOnlyBegin, dateOnlyEnd };
changeLogRepository
.GetDatesChange(
Arg.Any<Guid>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetDatesChange(discriminatorId, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetDatesChange(discriminatorId, CancellationToken.None);
}
private IEnumerable<ChangeLogValuesDto> GenerateChangeLogValuesDto(int count)
{
var items = new List<ChangeLogValuesDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogValuesDto()
{
Id = Uuid7.Guid(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
private IEnumerable<ChangeLogDto> GenerateChangeLogDto(int count)
{
var items = new List<ChangeLogDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogDto()
{
Id = Uuid7.Guid(),
});
}
return items;
}
}

View File

@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.API\DD.Persistence.API.csproj" />
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
</ItemGroup>

View File

@ -14,9 +14,10 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Получение данных на текущую дату (с пагинацией)
@ -47,55 +48,33 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<ChangeLogValuesDto>
/// <returns></returns>
Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment">комментарий</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Update(ChangeLogValuesDto dto, CancellationToken token);
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <param name="comment">комментарий</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Delete(Guid id, CancellationToken token);
Task<IActionResult> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, string comment, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <param name="comment">комментарий к удалению</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)

View File

@ -0,0 +1,22 @@
using DD.Persistence.Models.Requests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Repositories;
/// <summary>
/// Интерфейс для работы с коммитами журнала изменений
/// </summary>
public interface IChangeLogCommitRepository
{
/// <summary>
/// Добавить коммит для журнала изменений
/// </summary>
/// <param name="commitDto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<Guid> Add(CreateChangeLogCommitRequest commitDto, CancellationToken token);
}

View File

@ -1,61 +1,63 @@
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
namespace DD.Persistence.Repositories;
/// <summary>
/// Интерфейс для работы с историческими данными
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<ChangeLogValuesDto>
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="idAuthor">пользователь, который добавляет</param>
/// <param name="idDiscriminator">ключ справочника</param>
/// <param name="dto">коммит с изменениями</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> AddRange(Guid idDiscriminator, ChangeLogCommitDto dto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Пометить записи как удаленные
/// </summary>
/// <param name="idEditor"></param>
/// <param name="idCommit"></param>
/// <param name="ids">ключи записей</param>
/// <param name="updateTime"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token);
Task<int> MarkAsDeleted(Guid idCommit, IEnumerable<Guid> ids, DateTimeOffset updateTime, CancellationToken token);
/// <summary>
/// Пометить записи как удаленные
/// </summary>
/// <param name="idEditor"></param>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="idCommit"></param>
/// <param name="updateTime"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token);
Task<int> MarkAsDeleted(Guid idDiscriminator, Guid idCommit, DateTimeOffset updateTime, CancellationToken token);
/// <summary>
/// Очистить и добавить новые
/// </summary>
/// <param name="idAuthor"></param>
/// <param name="idDiscriminator"></param>
/// <param name="dto">коммит с изменениями</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> ClearAndAddRange(Guid idDiscriminator, ChangeLogCommitDto dto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Редактирование записей
/// </summary>
/// <param name="idEditor">пользователь, который редактирует</param>
/// <param name="commitDto">коммит с изменениями</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией)