Compare commits

..

117 Commits
dev ... master

Author SHA1 Message Date
726a51ea21 Верный конфиг для автотестов
All checks were successful
Unit tests / tests-and-publication (push) Successful in 1m54s
2025-03-07 15:50:39 +05:00
7d87723910 Merge pull request 'feature/#1037-change-log-statistics-2' (#33) from feature/#1037-change-log-statistics-2 into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m5s
Reviewed-on: #33
2025-03-07 15:45:44 +05:00
f99e0f0e54 Убран ChangeLogCreateRequest 2025-03-07 15:28:02 +05:00
919d7d5848 Правка по результатам ревью 2025-03-07 15:14:29 +05:00
Оля Бизюкова
afad83d409 Убрана модель HistoryChangeLogDto, добавлена фильтрация по датам, правки кода и автотестов по этому поводу 2025-03-06 14:04:33 +05:00
Оля Бизюкова
09eb397267 Merge branch 'feature/#1037-change-log-statistics-2' of ssh://git.ddrilling.ru:2221/on.nemtina/persistence into feature/#1037-change-log-statistics-2 2025-03-04 10:56:31 +05:00
Оля Бизюкова
70695c8091 Метод Get переименован на GetHistory 2025-03-04 10:56:06 +05:00
bf92e42469 Merge branch 'master' into feature/#1037-change-log-statistics-2 2025-03-04 10:50:26 +05:00
3fe03208c0 История изменений пользователя, сгруппированная по дням 2025-03-03 17:41:02 +05:00
3f4b9fbccf метод GetChangeLogForInterval теперь у ChangeLogCommitRepository, а не у ChangeLogRepository 2025-03-03 13:03:37 +05:00
c134f7ec09 Правка юнит-теста 2025-03-03 11:47:29 +05:00
ngfrolov
d5e7e00075
CreateChangeLogCommitCreateRequest -> ChangeLogCommitCreateRequest 2025-03-03 10:46:15 +05:00
7330f9b6db Merge pull request '#1004 Отделить логику маппинга от клиентов + #1000 Добавить множественный маппинг сущностей в TimeStampedClient' (#31) from feature-mapping into master
All checks were successful
Unit tests / tests-and-publication (push) Successful in 4m18s
Reviewed-on: #31
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2025-03-03 09:44:26 +05:00
ngfrolov
ebb2c6fade
ChangeLog тесты статистики работают.
Дискриминатор перенесен в таблицу с комитами.
Исправлены методы контроллера на добавление.
Исправлены методы клиента на добавление.
2025-02-28 17:41:34 +05:00
1e2fa287a0 Правки по результатам ревью 2025-02-28 13:15:27 +05:00
ngfrolov
6d9a7f754a
Merge remote-tracking branch 'origin/master' into feature/#1037-change-log-statistics-2 2025-02-27 17:07:31 +05:00
Оля Бизюкова
1756fac554 Интеграционные тесты 2025-02-27 16:56:03 +05:00
2dca1cc722 Исправление маппинга 2025-02-27 14:01:31 +05:00
81a542f54f Интеграционный тест + рефакторинг 2025-02-26 18:18:21 +05:00
Оля Бизюкова
fffd264533 Статистика по ChangeLog (продолжение) 2025-02-26 08:44:38 +05:00
d2c4db015b Фикс проекта с модульными тестами 2025-02-24 15:56:16 +05:00
cc4ceeff62 Merge branch 'master' into feature-mapping 2025-02-24 15:54:39 +05:00
0a0e7fa383 Правка интеграционных тестов для changeLog
All checks were successful
Unit tests / tests-and-publication (push) Successful in 2m17s
2025-02-24 15:51:15 +05:00
daeaf82b48 Статистика по changeLog. Начало 2025-02-24 15:38:59 +05:00
c5d552015d Merge branch 'master' into feature-mapping 2025-02-24 15:21:48 +05:00
efd7a7e639 Добавить множественный маппинг сущностей в TimeStampedClient 2025-02-24 15:21:02 +05:00
ba0c748659 Merge pull request 'Добавить таблицу для учета комментариев и действий пользователя для вывода статистики по ChangeLog' (#30) from feature/#956-change-log-table-comment into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m13s
Reviewed-on: #30
Reviewed-by: Никита Фролов <ng.frolov@digitaldrilling.ru>
2025-02-24 12:59:27 +05:00
ad3bc1a598 Merge branch 'master' into feature/#956-change-log-table-comment 2025-02-24 12:59:14 +05:00
a436b839f6 Правки по результатам ревью 2025-02-24 12:11:48 +05:00
0155624216 Merge branch 'master' into feature-mapping 2025-02-24 08:41:55 +05:00
930c4e8722 Комментарии к ChangeLogController 2025-02-21 14:22:34 +05:00
0c7006d52a нейминг-2 2025-02-21 12:28:32 +05:00
7590e8facb нейминг 2025-02-21 12:27:09 +05:00
a796024c69 Правки после ревью 2025-02-21 12:25:22 +05:00
Оля Бизюкова
4b7f55c3c8 Правка по результатам ревью 2025-02-20 18:30:56 +05:00
a5037b8967 Merge pull request '#1003 Убрать ненужные модификаторы virtual' (#29) from VirtualCleaning into master
All checks were successful
Unit tests / tests-and-publication (push) Successful in 2m29s
Reviewed-on: #29
2025-02-20 14:41:44 +05:00
Оля Бизюкова
523c15093f Мини-правки к автотестам 2025-02-20 14:40:20 +05:00
Оля Бизюкова
ba9fe675ed Дополнительные проверки в юнит-тесте UpdateRange для ChangeLog 2025-02-20 12:04:08 +05:00
c0d28efc70 Юнит-тесты 2025-02-19 17:44:48 +05:00
Оля Бизюкова
14893e9bf6 Правки по результатам ревью - 3 2025-02-18 17:21:30 +05:00
Оля Бизюкова
baba1a05c0 Правки по результатам PR - 2 2025-02-18 15:53:54 +05:00
Оля Бизюкова
fd8bfac6a6 Правки к PR - 1 2025-02-18 15:43:27 +05:00
Оля Бизюкова
0ec2e5d99f Правка после запуска интеграционных тестов 2025-02-18 11:17:04 +05:00
Оля Бизюкова
ebe4ec0cf6 Правка конфликтов по результатам мерджа из мастера 2025-02-18 11:07:41 +05:00
Оля Бизюкова
86d145c39f Merge from master 2025-02-18 11:01:21 +05:00
Оля Бизюкова
41b44efbb3 Корректный appsettings.json 2025-02-18 10:55:30 +05:00
Оля Бизюкова
da54d24745 Правки перед открытием PR к задаче по созданию таблицы с коммитами 2025-02-18 10:34:42 +05:00
05b58da1ab Интеграционные тесты 2025-02-17 17:34:07 +05:00
09cfccaa38 Приведение DD.Persistence.Repository.Test в исходное состояние 2025-02-17 11:23:43 +05:00
Оля Бизюкова
032d783d40 реализация методов ChangeLogService 2025-02-17 08:44:48 +05:00
c4b9878105 Добавить тест 2025-02-14 17:13:46 +05:00
58346b8f3e Merge branch 'master' into feature-mapping 2025-02-14 13:26:47 +05:00
3b4af1fd8d Наработки по ChangeLog 2025-02-13 17:57:43 +05:00
27b9728912 Убрать ненужные модификаторы virtual 2025-02-13 16:53:29 +05:00
93472f7933 Обновить DD.Persistence.API/Readme.md
All checks were successful
Unit tests / tests-and-publication (push) Successful in 1m56s
2025-02-13 13:27:58 +05:00
0d9c61c905 Обновить DD.Persistence.API/Readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:27:06 +05:00
36274b3a5e Обновить DD.Persistence.API/Readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:26:45 +05:00
b30d28dbeb Обновления в файле readme.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:23:00 +05:00
233850483c UML-диаграмма процесса пакетного редактирования
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-13 13:22:28 +05:00
5a44dfb109 Автотесты 2025-02-13 12:43:37 +05:00
ca0b1f0031 Убрана ссылка на удаленный проект с репозиториями из Dockerfile
All checks were successful
Unit tests / tests-and-publication (push) Successful in 5m16s
2025-02-12 16:43:34 +05:00
3cf7cbcc7c Фикс теста
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m22s
2025-02-12 16:33:42 +05:00
7c2ab3f6e2 Автотесты (начало) 2025-02-12 16:19:08 +05:00
77fa9d2c46 Исправлены тесты 2025-02-12 16:17:13 +05:00
a49a0f567a Merge pull request '#974 Модифицировать метод Get для TimestampedValues по части применения фильтра' (#28) from TimestampedValuesFilter into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m22s
Reviewed-on: #28
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2025-02-12 15:16:09 +05:00
c904c117d8 Правки после ревью #1 2025-02-12 15:15:43 +05:00
a9d0fa57f2 Маппинг отделен от клиентов 2025-02-12 15:14:14 +05:00
ebd2cff40d Поле с комментариями в методах контроллера, отвечающие за удаление 2025-02-12 11:24:31 +05:00
Оля Бизюкова
b3c6acbd18 Таблица в БД для учета коммитов в журнале изменений и всё, что с этим связано 2025-02-11 17:58:52 +05:00
0aca5d2d43 Добавить комментарий для клиента 2025-02-11 12:41:55 +05:00
4b9a4b4db7 Merge branch 'master' into TimestampedValuesFilter 2025-02-11 12:37:04 +05:00
e1f84f3091 Перед материализацией вычитываемых сущностей применить построенный фильтр 2025-02-11 12:34:37 +05:00
2fe369d49e Перевести TimestampedValuesRepository под спецификации 2025-02-10 17:25:45 +05:00
8e2c3a3a55 Добавить парсинг дерева в Get-запрос 2025-02-10 09:27:13 +05:00
52f7bca9f1 Merge pull request '#959 Реализовать обход бинарного дерева и создание фильтра на основе спецификаций' (#26) from FilterBuilder into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m6s
Reviewed-on: #26
2025-02-07 15:11:33 +05:00
2ef3efed33 Merge from dev 2025-02-07 15:11:09 +05:00
4739a043f4 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 56s
2025-02-07 12:30:59 +05:00
49cc9d6e39 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m1s
2025-02-07 12:15:35 +05:00
c997bfe9a4 Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m5s
2025-02-07 12:01:11 +05:00
fca2ccb8aa Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-07 12:00:23 +05:00
bfb18cab8a Обновить README.md
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-07 11:59:22 +05:00
c247489477 readme-файл, как запускать контейнер с persistence
Some checks failed
Unit tests / tests-and-publication (push) Failing after 57s
2025-02-07 11:57:07 +05:00
11eea8db67 Папка docker, где находится compose.yaml - файл для docker
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m5s
2025-02-07 11:33:45 +05:00
83c744939b Merge branch 'FilterBuilder' into FilterBinder 2025-02-07 08:33:40 +05:00
43289d939b Merge pull request 'fix/mock-methods-for-change-log' (#27) from fix/mock-methods-for-change-log into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m25s
Reviewed-on: #27
2025-02-06 17:23:32 +05:00
Оля Бизюкова
1f3df26e9a Правка по ревью 2025-02-06 17:21:28 +05:00
Оля Бизюкова
e0f0d9fdd0 Исправлен тип возвращаемых данных для GetStatisticsCountAsync и HistoryChangeLogAsync 2025-02-06 17:07:02 +05:00
Оля Бизюкова
ed6df68f7d Merge branch 'fix/mock-methods-for-change-log' of ssh://git.ddrilling.ru:2221/on.nemtina/persistence into fix/mock-methods-for-change-log 2025-02-06 16:57:31 +05:00
Оля Бизюкова
2ab8101258 Moq-данные для статистики и получения истории журнала изменений 2025-02-06 16:57:13 +05:00
d6038ca579 Merge branch 'master' into fix/mock-methods-for-change-log 2025-02-06 16:15:42 +05:00
Оля Бизюкова
86bf78f31f HistoryChangeLogDto для описания истории изменений 2025-02-06 16:14:02 +05:00
5ce5fa139b Merge pull request '#958 Перенести DD.Persistence.Repository + доработать схему данных' (#25) from RepositoriesRework into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 1m53s
Reviewed-on: #25
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2025-02-06 15:53:37 +05:00
272e164482 Merge branch 'master' into RepositoriesRework 2025-02-06 15:52:03 +05:00
aad8c57511 Исправить сломанный тест
Some checks failed
Unit tests / tests-and-publication (push) Has been cancelled
2025-02-06 15:50:04 +05:00
Оля Бизюкова
8fa661b605 Merge from dev
Some checks failed
Unit tests / tests-and-publication (push) Failing after 54s
2025-02-06 15:38:56 +05:00
Оля Бизюкова
979a651328 Merge from dev 2025-02-06 15:37:39 +05:00
baf04ae3a6 Merge pull request '#957 Реализовать построение дерева из строки' (#24) from TreeBuilder into master
Some checks failed
Unit tests / tests-and-publication (push) Failing after 2m56s
Reviewed-on: #24
2025-02-06 15:20:57 +05:00
598056c6d7 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 12:39:52 +05:00
c5da82c210 Правки по ревью #4 2025-02-06 12:39:40 +05:00
bcb9749b1a Правки по ревью #4 2025-02-06 12:32:28 +05:00
44bc335151 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 09:31:20 +05:00
7d973ba859 Правки по ревью #3 2025-02-06 09:31:10 +05:00
5abfcc0d50 Merge branch 'RepositoriesRework' into FilterBuilder 2025-02-06 09:25:08 +05:00
0c27a0148d Merge branch 'TreeBuilder' into FilterBuilder 2025-02-06 09:15:48 +05:00
a340fbe23b Наработка 2025-02-05 18:04:36 +05:00
9ca49cb1b5 Правки по ревью #2 2025-02-05 17:20:18 +05:00
431c7278cb Правки по ревью #1 2025-02-05 14:30:36 +05:00
4513de06fa Перенести содержимое проекта DD.Persistence.Repository в DD.Persistence.Database 2025-02-05 12:19:45 +05:00
63e6816e35 Правки по ревью #3 2025-02-05 12:10:01 +05:00
560073ed07 Правки по ревью #2 2025-02-05 11:45:47 +05:00
e3c1d02650 Правки по ревью #1 2025-02-05 10:58:26 +05:00
4b5477207d Реализовать обход бинарного дерева и создание фильтра на основе спецификаций 2025-02-05 10:40:34 +05:00
87264fd8db Merge branch 'TreeBuilder' into FilterBuilder 2025-02-05 10:17:08 +05:00
1d0921c3e8 Доработать схему данных по части хранения типов полей в соответствии с индексацией 2025-02-05 09:56:49 +05:00
f955aab218 Перенести содержимое проекта DD.Persistence.Repository в DD.Persistence.Database.Postgres 2025-02-05 09:16:28 +05:00
cb0508afe9 Убрать лишний комментарий 2025-02-05 08:48:20 +05:00
287d6e7111 Реализовать построение дерева из строки 2025-02-05 08:46:08 +05:00
132 changed files with 4734 additions and 1455 deletions

25
.docker/appsettings.json Normal file
View File

@ -0,0 +1,25 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=db:5432;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
},
"AllowedHosts": "*",
"NeedUseKeyCloak": false,
"KeyCloakAuthentication": {
"Audience": "account",
"Host": "http://192.168.0.10:8321/realms/Persistence"
},
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
},
"ClientUrl": "http://localhost:5000/"
}

31
.docker/compose.yaml Normal file
View File

@ -0,0 +1,31 @@
networks:
persistence:
external: false
services:
db:
image: timescale/timescaledb:latest-pg16
container_name: some-timescaledb-16
restart: always
environment:
- POSTGRES_PASSWORD=postgres
networks:
- persistence
ports:
- "5462:5432"
volumes:
- ./db:/var/lib/postgresql/data
persistence:
image: git.ddrilling.ru/ddrilling/persistence:latest
container_name: persistence
restart: always
depends_on:
- db
networks:
- persistence
ports:
- "1111:8080"
volumes:
- ./appsettings.json:/app/appsettings.json

View File

@ -1,133 +1,174 @@
using DD.Persistence.API;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using System.Net;
using DD.Persistence.Models.Common;
namespace DD.Persistence.API.Controllers;
/// <summary>
/// Контроллер по работе с журналом изменений
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class ChangeLogController : ControllerBase, IChangeLogApi
{
private readonly IChangeLogRepository repository;
private readonly ChangeLogService service;
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,
[FromBody] IEnumerable<IDictionary<string, object>> dtos,
string? comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token);
var changeLogCommitRequest = new ChangeLogCommitCreateRequest
{
IdAuthor = userId,
Comment = comment,
DiscriminatorId = idDiscriminator,
};
var result = await service.AddRange(changeLogCommitRequest, dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
/// <summary>
/// Удалить записи в журнале изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <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(Guid idDiscriminator, IEnumerable<Guid> ids, string comment, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, [id], token);
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, 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,
[FromBody] IEnumerable<IDictionary<string, object>> dtos,
string comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token);
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, userId, comment);
var result = await service.ClearAndAddRange(changeLogCommitRequest, dtos, token);
return Ok(result);
}
/// <summary>
/// сохранить изменения в записях журнала изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <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)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdateRange(
IEnumerable<ChangeLogValuesDto> dtos,
Guid idDiscriminator,
IEnumerable<ChangeLogBaseDto> dtos,
string comment,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token);
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, 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)]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogBaseDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent(
[FromRoute] Guid idDiscriminator,
[FromQuery] PaginationRequest paginationRequest,
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)]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogBaseDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset moment,
[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,41 +178,91 @@ 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(typeof(IEnumerable<ChangeLogBaseDto>), (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)
public async Task<ActionResult<IEnumerable<ChangeLogBaseDto>>> 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();
return Ok(result);
}
/// <summary>
/// Метод, который возвращает статистику пользователя по количеству изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("statistics")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogStatisticsDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetStatistics([FromQuery] ChangeLogQuery request, CancellationToken token)
{
var result = await service.GetStatistics(request, token);
return Ok(result);
}
/// <summary>
/// Метод, который возвращает историю изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogCommitDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetHistory([FromQuery] ChangeLogQuery request, CancellationToken token)
{
var result = await service.GetHistory(request, token);
return Ok(result);
}
}

View File

@ -1,4 +1,5 @@
using DD.Persistence.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces;
@ -45,6 +46,7 @@ public class TimestampedValuesController : ControllerBase
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
@ -52,9 +54,14 @@ public class TimestampedValuesController : ControllerBase
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromQuery] IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin, [FromQuery] string[]? columnNames, int skip, int take, CancellationToken token)
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromQuery] IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
[FromQuery] TNode? filterTree,
[FromQuery] string[]? columnNames,
int skip, int take,
CancellationToken token)
{
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, columnNames, skip, take, token);
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token);
return result.Any() ? Ok(result) : NoContent();
}

View File

@ -25,8 +25,11 @@
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Docs\" />
</ItemGroup>
</Project>

View File

@ -1,16 +1,14 @@
using Mapster;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
using DD.Persistence.Database.Entity;
namespace DD.Persistence.API;
@ -30,6 +28,7 @@ public static class DependencyInjection
new OpenApiSchema {Type = "number", Format = "float" }
]
});
c.MapType<TNode>(() => new OpenApiSchema { Type = "string" });
c.CustomOperationIds(e =>
{
@ -54,6 +53,7 @@ public static class DependencyInjection
{
services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
services.AddTransient<ChangeLogService>();
}
#region Authentication

View File

@ -0,0 +1,359 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" version="24.8.3">
<diagram name="Страница — 1" id="7k5Wemfp-yc9piGHxsiE">
<mxGraphModel dx="2049" dy="1054" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1300" pageHeight="1050" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_dnhcZFeje3u91oS2JwL-1" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="355" y="930" as="sourcePoint" />
<mxPoint x="355" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-2" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="930" as="sourcePoint" />
<mxPoint x="555" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-4" value="&lt;b&gt;FRONT&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="325" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-5" value="&lt;b&gt;BACK&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="525" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-7" value="&lt;b&gt;CACHE&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="725" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-8" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="955" y="930" as="sourcePoint" />
<mxPoint x="955" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-9" value="&lt;b&gt;COMMIT REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="880" y="180" width="155" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-32" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;align=center;" edge="1" parent="1" target="_dnhcZFeje3u91oS2JwL-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="930" as="sourcePoint" />
<mxPoint x="756.5000000000002" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-52" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="300" as="sourcePoint" />
<mxPoint x="755" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-53" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-52">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-67" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="310" as="sourcePoint" />
<mxPoint x="955" y="310.83" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-68" value="CreateCommit(userId, message)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-67">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-69" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="955" y="330" as="sourcePoint" />
<mxPoint x="755" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-70" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-69">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-88" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="930" as="sourcePoint" />
<mxPoint x="1155" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-89" value="&lt;b&gt;CHANGE LOG REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="1060" y="180" width="185" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-90" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="340" as="sourcePoint" />
<mxPoint x="555" y="340" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-91" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-90">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-93" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="380" as="sourcePoint" />
<mxPoint x="1155" y="380" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-94" value="AddRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-93">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-96" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="410" as="sourcePoint" />
<mxPoint x="555" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-97" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-96">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-100" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="355" y="420" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-101" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-100">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-102" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-104" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="290" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-105" value="Insert(items, message) [POST]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-104">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-107" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.13" y="420" as="sourcePoint" />
<mxPoint x="354.13" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-108" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="520" as="sourcePoint" />
<mxPoint x="555" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-109" value="Update(items, message) [PUT]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-108">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-110" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="530" as="sourcePoint" />
<mxPoint x="755" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-111" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-110">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-112" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="560" as="sourcePoint" />
<mxPoint x="555" y="560" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-113" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-112">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-114" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="650" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-115" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="610" as="sourcePoint" />
<mxPoint x="1155" y="610" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-116" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-115">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-117" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="640" as="sourcePoint" />
<mxPoint x="555" y="640" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-118" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-117">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-119" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="650" as="sourcePoint" />
<mxPoint x="355" y="650" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-120" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-119">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-121" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="530" as="sourcePoint" />
<mxPoint x="755" y="560" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="550" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-122" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="760" as="sourcePoint" />
<mxPoint x="555" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-123" value="Delete(items, message) [DELETE]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-122">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-124" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="770" as="sourcePoint" />
<mxPoint x="755" y="770" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-125" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-124">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-126" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="800" as="sourcePoint" />
<mxPoint x="555" y="800" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-127" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-126">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-128" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="890" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-129" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="850" as="sourcePoint" />
<mxPoint x="1155" y="850" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-130" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-129">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-131" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="880" as="sourcePoint" />
<mxPoint x="555" y="880" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-132" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-131">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-133" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="890" as="sourcePoint" />
<mxPoint x="355" y="890" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-134" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-133">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-135" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="770" as="sourcePoint" />
<mxPoint x="755" y="800" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="790" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-136" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="650" as="sourcePoint" />
<mxPoint x="354.57" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-137" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="890" as="sourcePoint" />
<mxPoint x="354.57" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-138" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="411" as="sourcePoint" />
<mxPoint x="1155" y="381" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-139" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="641" as="sourcePoint" />
<mxPoint x="1155" y="611" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-140" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1154.75" y="881" as="sourcePoint" />
<mxPoint x="1154.75" y="851" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-141" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="830" as="sourcePoint" />
<mxPoint x="754.93" y="312" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-142" value="" style="endArrow=none;html=1;rounded=0;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=10;opacity=50;gradientColor=#7ea6e0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165" y="882" as="sourcePoint" />
<mxPoint x="165.22" y="292" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-143" value="&lt;font color=&quot;#330066&quot;&gt;User&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;align=center;" vertex="1" parent="1">
<mxGeometry x="155" y="180" width="21.5" height="43" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-146" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="170" y="280" as="sourcePoint" />
<mxPoint x="360" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-147" value="SAVE" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-146">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-148" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165.22" y="930" as="sourcePoint" />
<mxPoint x="165.22" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-149" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="892" as="sourcePoint" />
<mxPoint x="165" y="892" as="targetPoint" />
<Array as="points">
<mxPoint x="265" y="892" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-150" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-149">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-152" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;UML-диаграмма процесса пакетного редактирования&lt;/h1&gt;&lt;p&gt;.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;align=center;" vertex="1" parent="1">
<mxGeometry x="75" y="50" width="1150" height="70" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-154" value="frame" style="shape=umlFrame;whiteSpace=wrap;html=1;pointerEvents=0;" vertex="1" parent="1">
<mxGeometry x="120" y="250" width="1120" height="680" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-156" value="&lt;p style=&quot;line-height: 120%;&quot;&gt;Время жизни кеша задается внутри проекта&lt;/p&gt;" style="shape=note;size=20;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#d0cee2;strokeColor=#56517e;opacity=50;" vertex="1" parent="1">
<mxGeometry x="760" y="660" width="120" height="100" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -48,4 +48,7 @@ Password: 12345
}
```
## Пакетное редактирование (на примере ChangeLog)
UML-диаграмма процесса редактирования находится по [ссылке](https://git.ddrilling.ru/on.nemtina/persistence/src/branch/master/DD.Persistence.API/Docs/ChangeLog_actions.drawio.xml)

View File

@ -1,6 +1,6 @@
using DD.Persistence.Database.Model;
using DD.Persistence.Database;
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Extensions;
using DD.Persistence.Repository;
namespace DD.Persistence.API;
@ -14,7 +14,7 @@ public class Startup
public void ConfigureServices(IServiceCollection services)
{
// Add services to the container.
// AddRange services to the container.
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -28,7 +28,6 @@ COPY ["DD.Persistence/DD.Persistence.csproj", "DD.Persistence/"]
COPY ["DD.Persistence.Database/DD.Persistence.Database.csproj", "DD.Persistence.Database/"]
COPY ["DD.Persistence.Database.Postgres/DD.Persistence.Database.Postgres.csproj", "DD.Persistence.Database.Postgres/"]
COPY ["DD.Persistence.Models/DD.Persistence.Models.csproj", "DD.Persistence.Models/"]
COPY ["DD.Persistence.Repository/DD.Persistence.Repository.csproj", "DD.Persistence.Repository/"]
RUN dotnet restore "./DD.Persistence.App/DD.Persistence.App.csproj"

View File

@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
"DefaultConnection": "Host=localhost;Port=5432;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
},
"AllowedHosts": "*",
"NeedUseKeyCloak": false,
@ -21,5 +21,5 @@
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
},
"PeristenceClientUrl": "http://localhost:5000/"
"ClientUrl": "http://localhost:5000/"
}

View File

@ -1,10 +1,10 @@
using Microsoft.Extensions.Logging;
using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.ChangeLog;
namespace DD.Persistence.Client.Clients;
/// <inheritdoc/>
@ -19,16 +19,16 @@ 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<IDictionary<string, object>> 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;
}
/// <inheritdoc/>
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment,
public async Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment,
PaginationRequest paginationRequest, CancellationToken token)
{
var result = await ExecuteGetResponse(
@ -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<IDictionary<string, object>> 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(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> dtos, string comment, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, token), token);
async () => await refitChangeLogClient.UpdateRange(idDiscriminator, dtos, comment, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> Update(ChangeLogValuesDto dto, CancellationToken token)
public async Task<int> DeleteRange(Guid idDiscriminator, 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(idDiscriminator, ids, comment, token), token);
return result;
}
@ -109,6 +82,24 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(ChangeLogQuery request, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.GetStatistics(request, token), token);
return result!;
}
/// <inheritdoc/>
public async Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.GetHistory(request, token), token);
return result!;
}
/// <inheritdoc/>
public void Dispose()
{
@ -116,4 +107,5 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
GC.SuppressFinalize(this);
}
}

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
@ -9,48 +9,35 @@ 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<IDictionary<string, object>> 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<IDictionary<string, object>> 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="idDiscriminator"></param>
/// <param name="ids"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(Guid idDiscriminator, IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
@ -60,7 +47,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token);
Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
@ -80,19 +67,29 @@ 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="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> dtos, string comment, 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="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(ChangeLogQuery request, CancellationToken token);
/// <summary>
/// Получение истории по журналу изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Client.Clients.Interfaces;
@ -24,28 +24,19 @@ public interface ITimestampedValuesClient : IDisposable
/// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</param>
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
string? filterTree,
IEnumerable<string>? columnNames,
int skip,
int take,
CancellationToken token);
/// <summary>
/// Получить данные с фильтрацией для нескольких систем. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="geTimestamp"></param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные, начиная с заданной отметки времени
/// </summary>
@ -94,13 +85,6 @@ public interface ITimestampedValuesClient : IDisposable
/// <param name="token"></param>
Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token);
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="idDiscriminator"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> GetLast<T>(Guid idDiscriminator, int take, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Refit;
@ -16,13 +16,13 @@ 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<IDictionary<string, object>> dtos, string comment, CancellationToken token);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
/// </summary>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<ChangeLogValuesDto>>> GetByDate(
Task<IApiResponse<PaginationContainer<ChangeLogBaseDto>>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
[Query] PaginationRequest paginationRequest,
@ -34,41 +34,27 @@ 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<IDictionary<string, object>> 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(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> 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(Guid idDiscriminator, [Body] IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
@ -76,4 +62,21 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
[Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получение статистики журнала изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[Get($"{BaseRoute}/statistics")]
Task<IApiResponse<IEnumerable<ChangeLogStatisticsDto>>> GetStatistics(ChangeLogQuery request, CancellationToken token);
/// <summary>
/// Получение истории по журналу изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[Get($"{BaseRoute}/history")]
Task<IApiResponse<IEnumerable<ChangeLogCommitDto>>> GetHistory(ChangeLogQuery request, CancellationToken token);
}

View File

@ -23,6 +23,7 @@ public interface IRefitTimestampedValuesClient : IRefitClient, IDisposable
[Get($"{baseUrl}")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> Get([Query(CollectionFormat.Multi)] IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
[Query] string? filterTree,
[Query(CollectionFormat.Multi)] IEnumerable<string>? columnNames,
int skip,
int take,

View File

@ -0,0 +1,6 @@
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
internal interface IMapperStorage
{
TimestampedSetMapper GetMapper<T>(Guid idDiscriminator);
TimestampedSetMapper? GetMapper(Guid idDiscriminator);
}

View File

@ -0,0 +1,6 @@
using DD.Persistence.Client.Clients.Interfaces;
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
public interface ISetpointMappingClient : ISetpointClient
{
}

View File

@ -0,0 +1,45 @@
using DD.Persistence.Client.Clients.Interfaces;
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
/// <summary>
/// Маппинг - обертка для клиента по работе с данными
/// </summary>
public interface ITimestampedMappingClient : ITimestampedValuesClient
{
/// <summary>
/// Получить данные с преобразованием к заданному типу
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="geTimestamp"></param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="filterTree"></param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<T>> GetMapped<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить набор данных, преобразованных к соответствующим типам из заданного конфига
/// </summary>
/// <param name="discriminatorIds"></param>
/// <param name="timestampBegin"></param>
/// <param name="filterTree"></param>
/// <param name="columnNames"></param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IDictionary<Guid, IEnumerable<object>>> GetMultiMapped(IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные с конца с преобразованием к заданному типу
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="idDiscriminator"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> GetLastMapped<T>(Guid idDiscriminator, int take, CancellationToken token);
}

View File

@ -0,0 +1,103 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Configurations;
using System.Text.Json;
namespace DD.Persistence.Client.Clients.Mapping.Clients;
/// <inheritdoc/>
public class SetpointMappingClient : ISetpointMappingClient
{
private readonly ISetpointClient setpointClient;
private readonly MappingConfig mappingConfigs;
/// <inheritdoc/>
public SetpointMappingClient(ISetpointClient setpointClient, MappingConfig mappingConfigs)
{
this.setpointClient = setpointClient;
this.mappingConfigs = mappingConfigs;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
=> (await setpointClient.GetCurrent(setpointKeys, token))
.Select(x => new SetpointValueDto
{
Key = x.Key,
Value = DeserializeValue(x.Key, (JsonElement)x.Value)
});
/// <inheritdoc/>
public async Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointConfigs, CancellationToken token)
{
var result = (await setpointClient.GetCurrent(setpointConfigs, token))
.ToDictionary(x => x.Key, x => DeserializeValue(x.Key, (JsonElement)x.Value));
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
{
var result = await setpointClient.GetHistory(setpointKeys, historyMoment, token);
foreach (var dto in result)
dto.Value = DeserializeValue(dto.Key, (JsonElement)dto.Value);
return result;
}
/// <inheritdoc/>
public async Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointClient.GetLog(setpointKeys, token);
foreach (var item in result)
DeserializeList(result[item.Key]);
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointLogDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token)
{
var result = await setpointClient.GetPart(dateBegin, take, token);
DeserializeList(result);
return result;
}
/// <inheritdoc/>
public async Task Add(Guid setpointKey, object newValue, CancellationToken token)
=> await setpointClient.Add(setpointKey, newValue, token);
/// <inheritdoc/>
public async Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token)
=> await setpointClient.GetDatesRangeAsync(token);
/// <inheritdoc/>
public void Dispose()
{
setpointClient.Dispose();
}
private object DeserializeValue(Guid key, JsonElement value)
{
if (mappingConfigs.TryGetValue(key, out var type))
return value.Deserialize(type)!;
return value;
}
private void DeserializeList(IEnumerable<SetpointLogDto>? result)
{
if (result is null)
return;
foreach (var log in result)
log.Value = DeserializeValue(log.Key, (JsonElement)log.Value);
}
}

View File

@ -0,0 +1,107 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using Microsoft.Extensions.Logging;
namespace DD.Persistence.Client.Clients.Mapping.Clients;
/// <inheritdoc/>
internal class TimestampedMappingClient : ITimestampedMappingClient
{
private readonly ITimestampedValuesClient client;
private readonly IMapperStorage mapperStorage;
public TimestampedMappingClient(ITimestampedValuesClient client, IMapperStorage mapperStorage)
{
this.client = client;
this.mapperStorage = mapperStorage;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetMapped<T>(Guid discriminatorId, DateTimeOffset? geTimestamp,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get([discriminatorId], geTimestamp, filterTree, columnNames, skip, take, token);
var mapper = mapperStorage.GetMapper<T>(discriminatorId);
var mappedDtos = data.Select(mapper.DeserializeTimeStampedData).OfType<T>();
return mappedDtos;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetLastMapped<T>(Guid idDiscriminator, int take, CancellationToken token)
{
var data = await GetLast(idDiscriminator, take, token);
var mapper = mapperStorage.GetMapper<T>(idDiscriminator);
var mappedDtos = data.Select(mapper.DeserializeTimeStampedData).OfType<T>();
return mappedDtos;
}
/// <inheritdoc/>
public async Task<IDictionary<Guid, IEnumerable<object>>> GetMultiMapped(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await client.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token);
var result = discriminatorIds
.ToDictionary(discriminatorId => discriminatorId, discriminatorId =>
{
var mapper = mapperStorage.GetMapper(discriminatorId);
ArgumentNullException.ThrowIfNull(mapper);
var mappedDtos = data
.Where(e => e.DiscriminatorId == discriminatorId)
.Select(mapper.DeserializeTimeStampedData);
return mappedDtos;
});
return result;
}
/// <inheritdoc/>
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
=> await client.AddRange(discriminatorId, dtos, token);
/// <inheritdoc/>
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
=> await client.Count(discriminatorId, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
=> await client.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token);
/// <inheritdoc/>
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
=> await client.GetDatesRange(discriminatorId, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int take, CancellationToken token)
=> await client.GetFirst(discriminatorId, take, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
=> await client.GetGtDate(discriminatorId, timestampBegin, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int take, CancellationToken token)
=> await client.GetLast(discriminatorId, take, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default)
=> await client.GetResampledData(discriminatorId, timestampBegin, intervalSec, approxPointsCount, token);
/// <inheritdoc/>
public void Dispose()
{
client.Dispose();
}
}

View File

@ -0,0 +1,33 @@
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
namespace DD.Persistence.Client.Clients.Mapping;
internal class MapperStorage : IMapperStorage
{
private readonly ConcurrentDictionary<Guid, TimestampedSetMapper> mapperCache = new();
private readonly MappingConfig mappingConfigs;
private readonly ILogger<TimestampedSetMapper> logger;
public MapperStorage(MappingConfig mappingConfigs, ILogger<TimestampedSetMapper> logger)
{
this.mappingConfigs = mappingConfigs;
this.logger = logger;
}
public TimestampedSetMapper? GetMapper(Guid idDiscriminator)
{
if (mappingConfigs.TryGetValue(idDiscriminator, out var type))
return mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper(idDiscriminator, type, logger));
return null;
}
public TimestampedSetMapper GetMapper<T>(Guid idDiscriminator)
{
return mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper(idDiscriminator, typeof(T), logger));
}
}

View File

@ -1,65 +1,62 @@
using DD.Persistence.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
namespace DD.Persistence.Client;
namespace DD.Persistence.Client.Clients.Mapping;
internal abstract class TimestampedSetMapperBase
internal class TimestampedSetMapper
{
public abstract object Map(TimestampedValuesDto data);
private readonly Type entityType;
private readonly ILogger<TimestampedSetMapper> logger;
}
internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
{
private readonly Type entityType = typeof(T);
public Guid IdDiscriminator { get; }
private readonly ConcurrentDictionary<string, PropertyInfo?> PropertyCache = new();
public TimestampedSetMapper(Guid idDiscriminator)
public TimestampedSetMapper(Guid idDiscriminator, Type entityType, ILogger<TimestampedSetMapper> logger)
{
IdDiscriminator = idDiscriminator;
this.entityType = entityType;
this.logger = logger;
}
public override object Map(TimestampedValuesDto data)
public object DeserializeTimeStampedData(TimestampedValuesDto data)
{
return DeserializeTimeStampedData(data)!;
}
public T DeserializeTimeStampedData(TimestampedValuesDto data)
{
if (entityType.IsValueType)
return MapStruct(data);
else
return MapClass(data);
return MapClass(data);
}
private T MapClass(TimestampedValuesDto data)
private object MapClass(TimestampedValuesDto data)
{
var entity = (T)RuntimeHelpers.GetUninitializedObject(typeof(T));
var entity = RuntimeHelpers.GetUninitializedObject(entityType);
foreach (var (propertyName, value) in data.Values)
{
if (value is JsonElement jsonElement)
SetPropertyValueFromJson(ref entity, propertyName, jsonElement);
}
SetPropertyValue(ref entity, "Timestamp", data.Timestamp);
SetPropertyValue(ref entity, nameof(TimestampedValuesDto.Timestamp), data.Timestamp);
SetPropertyValue(ref entity, nameof(TimestampedValuesDto.DiscriminatorId), data.DiscriminatorId);
return entity;
}
private T MapStruct(TimestampedValuesDto data)
private object MapStruct(TimestampedValuesDto data)
{
var entity = Activator.CreateInstance<T>();
var entity = Activator.CreateInstance(entityType);
object boxedEntity = entity!;
foreach (var (propertyName, value) in data.Values)
{
if (value is JsonElement jsonElement)
SetPropertyValueForStructFromJson(ref boxedEntity, propertyName, jsonElement);
}
SetPropertyValueForStruct(ref boxedEntity, "Timestamp", data.Timestamp);
SetPropertyValueForStruct(ref boxedEntity, nameof(TimestampedValuesDto.Timestamp), data.Timestamp);
SetPropertyValueForStruct(ref boxedEntity, nameof(TimestampedValuesDto.DiscriminatorId), data.DiscriminatorId);
return (T)boxedEntity;
return boxedEntity;
}
private void SetPropertyValueForStructFromJson(ref object entity, string propertyName, JsonElement element)
@ -73,8 +70,9 @@ internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
var value = element.Deserialize(property.PropertyType);
property.SetValue(entity, value);
}
catch (Exception ex)
catch (Exception ex)
{
logger.LogError(ex.Message);
}
}
private void SetPropertyValueForStruct(ref object entity, string propertyName, object value)
@ -88,13 +86,14 @@ internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(entity, convertedValue);
}
catch (Exception ex)
catch (Exception ex)
{
logger.LogError(ex.Message);
}
}
private void SetPropertyValueFromJson(ref T entity, string propertyName, JsonElement jsonElement)
private void SetPropertyValueFromJson(ref object entity, string propertyName, JsonElement jsonElement)
{
var property = GetPropertyInfo(propertyName);
@ -106,13 +105,13 @@ internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
var value = jsonElement.Deserialize(property.PropertyType);
property.SetValue(entity, value);
}
catch (Exception ex)
catch (Exception ex)
{
logger.LogError(ex.Message);
}
}
private void SetPropertyValue(ref T entity, string propertyName, object value)
private void SetPropertyValue(ref object entity, string propertyName, object value)
{
var property = GetPropertyInfo(propertyName);
if (property is null)
@ -123,8 +122,9 @@ internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
var convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(entity, convertedValue);
}
catch (Exception ex)
catch (Exception ex)
{
logger.LogError(ex.Message);
}
}

View File

@ -3,9 +3,6 @@ using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Client.Clients;
@ -13,15 +10,12 @@ namespace DD.Persistence.Client.Clients;
public class SetpointClient : BaseClient, ISetpointClient
{
private readonly IRefitSetpointClient refitSetpointClient;
private readonly ISetpointConfigStorage setpointConfigStorage;
public SetpointClient(
IRefitClientFactory<IRefitSetpointClient> refitSetpointClientFactory,
ISetpointConfigStorage setpointConfigStorage,
ILogger<SetpointClient> logger) : base(logger)
{
this.refitSetpointClient = refitSetpointClientFactory.Create();
this.setpointConfigStorage = setpointConfigStorage;
}
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
@ -31,7 +25,7 @@ public class SetpointClient : BaseClient, ISetpointClient
return result!.Select(x => new SetpointValueDto {
Key = x.Key,
Value = DeserializeValue(x.Key, x.Value)
Value = x.Value
});
}
@ -43,16 +37,13 @@ public class SetpointClient : BaseClient, ISetpointClient
async () => await refitSetpointClient.GetCurrent(setpointConfigs, token), token);
return result!.ToDictionary(x => x.Key,x => DeserializeValue(x.Key,x.Value));
return result!.ToDictionary(x => x.Key,x => (object)x.Value);
}
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetHistory(setpointKeys, historyMoment, token), token);
foreach(var dto in result)
dto.Value = DeserializeValue(dto.Key, (JsonElement)dto.Value);
return result!;
@ -63,9 +54,6 @@ public class SetpointClient : BaseClient, ISetpointClient
var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetLog(setpointKeys, token), token);
foreach(var item in result)
DeserializeList(result[item.Key]);
return result!;
}
@ -82,8 +70,6 @@ public class SetpointClient : BaseClient, ISetpointClient
var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetPart(dateBegin, take, token), token);
DeserializeList(result);
return result!;
}
@ -101,21 +87,4 @@ public class SetpointClient : BaseClient, ISetpointClient
GC.SuppressFinalize(this);
}
private object DeserializeValue(Guid key, JsonElement value)
{
if (setpointConfigStorage.TryGetType(key, out var type))
return value.Deserialize(type)!;
return value;
}
private void DeserializeList(IEnumerable<SetpointLogDto>? result)
{
foreach (var log in result)
log.Value = DeserializeValue(log.Key, (JsonElement)log.Value);
}
}

View File

@ -1,4 +1,4 @@
using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models;
@ -19,8 +19,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
this.refitTimestampedSetClient = refitTimestampedSetClientFactory.Create();
}
/// <inheritdoc/>
private readonly ConcurrentDictionary<Guid, TimestampedSetMapperBase> mapperCache = new();
/// <inheritdoc/>
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> sets, CancellationToken token)
@ -32,22 +31,13 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token), token);
async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token), token);
return result!;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get([discriminatorId], geTimestamp, columnNames, skip, take, token);
var mapper = GetMapper<T>(discriminatorId);
return data.Select(mapper.DeserializeTimeStampedData);
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{
@ -102,20 +92,7 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetLast<T>(Guid idDiscriminator, int take, CancellationToken token)
{
var data = await GetLast(idDiscriminator, take, token);
var mapper = GetMapper<T>(idDiscriminator);
return data.Select(mapper.DeserializeTimeStampedData);
}
/// <inheritdoc/>
private TimestampedSetMapper<T> GetMapper<T>(Guid idDiscriminator)
{
return (TimestampedSetMapper<T>)mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper<T>(idDiscriminator));
}
/// <inheritdoc/>
public void Dispose()

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Client.CustomExceptions;
/// <summary>
/// Ошибка, которая выбрасывается в случае отсутствия необходимого свойства в appsettings.json
/// </summary>
public class AppSettingsPropertyNotFoundException : Exception
{
/// <summary>
///
/// </summary>
/// <param name="message"></param>
public AppSettingsPropertyNotFoundException(string message)
: base(message) { }
}

View File

@ -52,6 +52,7 @@
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>

View File

@ -1,6 +1,10 @@
using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.DependencyInjection;
namespace DD.Persistence.Client;
@ -15,7 +19,7 @@ public static class DependencyInjection
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddPersistenceClients(this IServiceCollection services, Dictionary<Guid, Type>? setpointTypeConfigs = null)
public static IServiceCollection AddPersistenceClients(this IServiceCollection services)
{
services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>));
services.AddTransient<IChangeLogClient, ChangeLogClient>();
@ -25,10 +29,17 @@ public static class DependencyInjection
services.AddTransient<ITimestampedValuesClient, TimestampedValuesClient>();
services.AddTransient<IWitsDataClient, WitsDataClient>();
services.AddSingleton<ISetpointConfigStorage, SetpointConfigStorage>(provider =>
{
return new SetpointConfigStorage(setpointTypeConfigs);
});
return services;
}
public static IServiceCollection AddPersistenceMapping(this IServiceCollection services, MappingConfig mappingConfigs)
{
services.AddSingleton(mappingConfigs);
services.AddSingleton<IMapperStorage, MapperStorage>();
services.AddTransient<ISetpointMappingClient, SetpointMappingClient>();
services.AddTransient<ITimestampedMappingClient, TimestampedMappingClient>();
return services;
}
}

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Client;
public interface ISetpointConfigStorage
{
bool TryGetType(Guid id, out Type type);
}

View File

@ -1,8 +1,9 @@
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Client.CustomExceptions;
using DD.Persistence.Client.Helpers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Refit;
using System.Configuration;
using System.Text.Json;
namespace DD.Persistence.Client;
@ -22,10 +23,10 @@ public class RefitClientFactory<T> : IRefitClientFactory<T> where T : IRefitClie
//this.client = factory.CreateClient();
this.client = client;
var baseUrl = configuration.GetSection("PeristenceClientUrl").Get<string>();
var baseUrl = configuration.GetSection("ClientUrl").Get<string>();
if (String.IsNullOrEmpty(baseUrl))
{
var exception = new AppSettingsPropertyNotFoundException("В настройках конфигурации не указан адрес Persistence сервиса.");
var exception = new SettingsPropertyNotFoundException("В настройках конфигурации не указан адрес Persistence сервиса.");
logger.LogError(exception.Message);

View File

@ -1,20 +0,0 @@
namespace DD.Persistence.Client;
internal class SetpointConfigStorage : ISetpointConfigStorage
{
private readonly Dictionary<Guid, Type> setpointTypeConfigs;
public SetpointConfigStorage(Dictionary<Guid, Type>? setpointTypeConfigs)
{
this.setpointTypeConfigs = setpointTypeConfigs?? new Dictionary<Guid, Type>();
}
public bool TryGetType(Guid id, out Type type)
{
return setpointTypeConfigs.TryGetValue(id, out type);
}
public void AddOrReplace(Guid id, Type type)
{
setpointTypeConfigs[id] = type;
}
}

View File

@ -1,6 +1,13 @@
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database.Model;

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistencePostgresContext))]
[Migration("20250203061429_Init")]
partial class Init
[Migration("20250228042803_Innit")]
partial class Innit
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -33,29 +33,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid")
.HasComment("Ключ записи");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("IdAuthor")
b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
.HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<string>("Value")
.IsRequired()
@ -64,24 +52,40 @@ 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.DataScheme", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("DiscriminatorId")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
.HasComment("Id коммита");
b.Property<string>("PropNames")
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.HasKey("DiscriminatorId");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.ToTable("data_scheme");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -129,6 +133,30 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{
b.Property<Guid>("Key")
@ -207,6 +235,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")
@ -218,15 +263,11 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChangeLogCreatedItems");
b.Navigation("DataScheme");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618
}

View File

@ -7,39 +7,24 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace DD.Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
public partial class Innit : Migration
{
/// <inheritdoc />
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: "Ключ записи"),
IdDiscriminator = 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: "Пользователь, создавший коммит"),
DiscriminatorId = 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);
});
migrationBuilder.CreateTable(
name: "data_scheme",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
PropNames = table.Column<string>(type: "jsonb", nullable: false, comment: "Наименования полей в порядке индексации")
},
constraints: table =>
{
table.PrimaryKey("PK_data_scheme", x => x.DiscriminatorId);
table.PrimaryKey("PK_change_log_commit", x => x.Id);
});
migrationBuilder.CreateTable(
@ -69,6 +54,20 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp });
});
migrationBuilder.CreateTable(
name: "scheme_property",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
Index = table.Column<int>(type: "integer", nullable: false, comment: "Индекс поля"),
PropertyName = table.Column<string>(type: "text", nullable: false, comment: "Наименования индексируемого поля"),
PropertyKind = table.Column<byte>(type: "smallint", nullable: false, comment: "Тип индексируемого поля")
},
constraints: table =>
{
table.PrimaryKey("PK_scheme_property", x => new { x.DiscriminatorId, x.Index });
});
migrationBuilder.CreateTable(
name: "setpoint",
columns: table => new
@ -94,12 +93,32 @@ namespace DD.Persistence.Database.Postgres.Migrations
constraints: table =>
{
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: "Ключ записи"),
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_timestamped_values_data_scheme_DiscriminatorId",
column: x => x.DiscriminatorId,
principalTable: "data_scheme",
principalColumn: "DiscriminatorId",
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(
@ -124,6 +143,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",
@ -139,6 +168,9 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable(
name: "parameter_data");
migrationBuilder.DropTable(
name: "scheme_property");
migrationBuilder.DropTable(
name: "setpoint");
@ -149,10 +181,10 @@ namespace DD.Persistence.Database.Postgres.Migrations
name: "timestamped_values");
migrationBuilder.DropTable(
name: "data_source_system");
name: "change_log_commit");
migrationBuilder.DropTable(
name: "data_scheme");
name: "data_source_system");
}
}
}

View File

@ -30,29 +30,17 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid")
.HasComment("Ключ записи");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("IdAuthor")
b.Property<Guid>("IdCreatedCommit")
.HasColumnType("uuid")
.HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
.HasComment("Id коммита на создание записи");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasComment("Id заменяющей записи");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasComment("Дата устаревания (например при удалении)");
b.Property<Guid?>("IdObsoletedCommit")
.HasColumnType("uuid")
.HasComment("Id коммита на устаревание записи");
b.Property<string>("Value")
.IsRequired()
@ -61,24 +49,40 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id");
b.ToTable("change_log");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.Property<Guid>("DiscriminatorId")
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
.HasComment("Id коммита");
b.Property<string>("PropNames")
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Наименования полей в порядке индексации");
.HasColumnType("text")
.HasComment("Комментарий к коммиту");
b.HasKey("DiscriminatorId");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.ToTable("data_scheme");
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -99,7 +103,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("SystemId");
b.ToTable("data_source_system");
b.ToTable("data_source_system", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ParameterData", b =>
@ -123,7 +127,31 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("DiscriminatorId", "ParameterId", "Timestamp");
b.ToTable("parameter_data");
b.ToTable("parameter_data", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
@ -146,7 +174,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Key", "Timestamp");
b.ToTable("setpoint");
b.ToTable("setpoint", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
@ -181,7 +209,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasIndex("SystemId");
b.ToTable("tech_message");
b.ToTable("tech_message", (string)null);
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
@ -201,7 +229,24 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("DiscriminatorId", "Timestamp");
b.ToTable("timestamped_values");
b.ToTable("timestamped_values", (string)null);
});
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 =>
@ -215,15 +260,11 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b =>
{
b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChangeLogCreatedItems");
b.Navigation("DataScheme");
b.Navigation("ChangeLogObsoletedItems");
});
#pragma warning restore 612, 618
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -7,11 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,44 +1,35 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Database.Repositories;
using DD.Persistence.Database.RepositoriesCached;
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Repositories;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Repository.RepositoriesCached;
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Repository;
namespace DD.Persistence.Database;
public static class DependencyInjection
{
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogValuesDto()
{
Value = src.Value,
Id = src.Id
});
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.RuleMap.Clear();
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
MapsterSetup();
MapsterSetup.Configure();
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogCommitRepository, ChangeLogCommitRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<IDataSchemeRepository, DataSchemeCachedRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<ISchemePropertyRepository, SchemePropertyCachedRepository>();
return services;
}

View File

@ -1,6 +1,4 @@
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.ModelsAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@ -11,29 +9,32 @@ namespace DD.Persistence.Database.Entity;
/// Часть записи, описывающая изменение
/// </summary>
[Table("change_log")]
public class ChangeLog : IChangeLog
public class ChangeLog
{
[Key, Comment("Ключ записи")]
public Guid Id { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid IdDiscriminator { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
[Comment("Редактор")]
public Guid? IdEditor { get; set; }
[Comment("Дата создания записи")]
public DateTimeOffset Creation { get; set; }
[Comment("Дата устаревания (например при удалении)")]
public DateTimeOffset? Obsolete { get; set; }
[Comment("Id заменяющей записи")]
public Guid? IdNext { get; set; }
[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; }
/// <summary>
/// коммит для актуальной записи
/// </summary>
[Required, ForeignKey(nameof(IdCreatedCommit)), Comment("Коммит пользователя на создание записи")]
public virtual ChangeLogCommit CreatedCommit { get; set; } = null!;
/// <summary>
/// коммит для устаревшей записи
/// </summary>
[ForeignKey(nameof(IdObsoletedCommit)), Comment("Коммит пользователя на устаревание записи")]
public virtual ChangeLogCommit? ObsoletedCommit { get; set; }
}

View File

@ -0,0 +1,34 @@
using DD.Persistence.Database.EntityAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
/// <summary>
/// Таблица c коммитами пользователей
/// </summary>
[Table("change_log_commit")]
public class ChangeLogCommit: IDiscriminatorItem
{
[Key, Comment("Id коммита")]
public Guid Id { get; set; }
[Comment("Пользователь, создавший коммит")]
public Guid IdAuthor { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid DiscriminatorId { 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!;
[Required, InverseProperty(nameof(ChangeLog.ObsoletedCommit)), Comment("Устаревшие записи в журнале изменений")]
public virtual ICollection<ChangeLog> ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -1,15 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
[Table("data_scheme")]
public class DataScheme
{
[Key, Comment("Идентификатор схемы данных"),]
public Guid DiscriminatorId { get; set; }
[Comment("Наименования полей в порядке индексации"), Column(TypeName = "jsonb")]
public string[] PropNames { get; set; } = [];
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("parameter_data")]
[PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))]
public class ParameterData : ITimestampedItem
public class ParameterData : IDiscriminatorItem, ITimestampedItem
{
[Required, Comment("Дискриминатор системы")]
public Guid DiscriminatorId { get; set; }

View File

@ -0,0 +1,24 @@
using DD.Persistence.Database.EntityAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace DD.Persistence.Database.Entity;
[Table("scheme_property")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Index))]
public class SchemeProperty : IDiscriminatorItem
{
[Comment("Идентификатор схемы данных")]
public Guid DiscriminatorId { get; set; }
[Comment("Индекс поля")]
public int Index { get; set; }
[Comment("Наименования индексируемого поля")]
public required string PropertyName { get; set; }
[Comment("Тип индексируемого поля")]
public required JsonValueKind PropertyKind { get; set; }
}

View File

@ -17,15 +17,15 @@ public class TechMessage : ITimestampedItem
[Comment("Дата возникновения")]
public DateTimeOffset Timestamp { get; set; }
[Column(TypeName = "varchar(512)"), Comment("Текст сообщения")]
public required string Text { get; set; }
[Column(TypeName = "varchar(512)"), Comment("Текст сообщения")]
public required string Text { get; set; }
[Required, Comment("Id системы, к которой относится сообщение")]
public required Guid SystemId { get; set; }
[Required, Comment("Id системы, к которой относится сообщение")]
public required Guid SystemId { get; set; }
[Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")]
public virtual required DataSourceSystem System { get; set; }
[Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")]
public virtual required DataSourceSystem System { get; set; }
[Comment("Статус события")]
public int EventState { get; set; }
}
[Comment("Статус события")]
public int EventState { get; set; }
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("timestamped_values")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))]
public class TimestampedValues : ITimestampedItem
public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesItem
{
[Comment("Временная отметка"), Key]
public DateTimeOffset Timestamp { get; set; }
@ -17,7 +17,4 @@ public class TimestampedValues : ITimestampedItem
[Comment("Данные"), Column(TypeName = "jsonb")]
public required object[] Values { get; set; }
[Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")]
public virtual DataScheme? DataScheme { get; set; }
}

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>
@ -30,18 +20,4 @@ public interface IChangeLog
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id заменяющей записи
/// </summary>
public Guid? IdNext { get; set; }
/// <summary>
/// Дискриминатор таблицы
/// </summary>
public Guid IdDiscriminator { get; set; }
/// <summary>
/// Значение
/// </summary>
public IDictionary<string, object> Value { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace DD.Persistence.Database.EntityAbstractions;
public interface IDiscriminatorItem
{
/// <summary>
/// Дискриминатор
/// </summary>
Guid DiscriminatorId { get; set; }
}

View File

@ -0,0 +1,14 @@
using DD.Persistence.Database.Entity;
namespace DD.Persistence.Database.EntityAbstractions;
/// <summary>
/// Сущность с данными, принадлежащими к определенной схеме
/// </summary>
public interface IValuesItem
{
/// <summary>
/// Значения
/// </summary>
object[] Values { get; set; }
}

View File

@ -2,7 +2,7 @@ using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
namespace DD.Persistence.Repository.Extensions;
namespace DD.Persistence.Database.Extensions;
public static class EFExtensionsSortBy
{

View File

@ -0,0 +1,38 @@
using Ardalis.Specification;
using DD.Persistence.Database.Helpers;
using System.Linq.Expressions;
namespace DD.Persistence.Database.Postgres.Extensions;
public static class SpecificationExtensions
{
public static Expression<Func<T, bool>>? Or<T>(this ISpecification<T> spec, ISpecification<T> otherSpec)
{
var parameter = Expression.Parameter(typeof(T), "x");
var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter);
var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter);
Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null
? Expression.OrElse(exprSpec1, exprSpec2)
: exprSpec1 ?? exprSpec2;
if (orExpression is null)
return null;
var lambdaExpr = Expression.Lambda<Func<T, bool>>(orExpression, parameter);
return lambdaExpr;
}
public static Expression? CombineWhereExpressions<T>(IEnumerable<WhereExpressionInfo<T>> whereExpressions, ParameterExpression parameter)
{
Expression? newExpr = null;
foreach (var where in whereExpressions)
{
var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter);
newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr);
}
return newExpr;
}
}

View File

@ -1,6 +1,6 @@
using System.Collections;
namespace DD.Persistence.Repository;
namespace DD.Persistence.Database.Postgres.Helpers;
/// <summary>
/// Цикличный массив
/// </summary>

View File

@ -0,0 +1,116 @@
using Ardalis.Specification;
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Database.Specifications.Operation;
using DD.Persistence.Database.Specifications.ValuesItem;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.Visitors;
using DD.Persistence.Models;
using System.Text.Json;
namespace DD.Persistence.Database.Postgres.Helpers;
public static class FilterBuilder
{
public static IQueryable<TEntity> ApplyFilter<TEntity>(this IQueryable<TEntity> query, DataSchemeDto dataSchemeDto, TNode root)
where TEntity : class, IValuesItem
{
var filterSpec = dataSchemeDto.BuildFilter<TEntity>(root);
if (filterSpec != null)
return query.WithSpecification(filterSpec);
return query;
}
private static ISpecification<TEntity>? BuildFilter<TEntity>(this DataSchemeDto dataSchemeDto, TNode root)
where TEntity : IValuesItem
{
var result = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(root);
return result;
}
private static ISpecification<TEntity>? BuildSpecificationByNextNode<TEntity>(this DataSchemeDto dataSchemeDto, TNode node)
where TEntity : IValuesItem
{
var visitor = new NodeVisitor<ISpecification<TEntity>?>(
dataSchemeDto.VertexProcessing<TEntity>,
dataSchemeDto.LeafProcessing<TEntity>
);
var result = node.AcceptVisitor(visitor);
return result;
}
private static ISpecification<TEntity>? VertexProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TVertex vertex)
where TEntity : IValuesItem
{
var leftSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Left);
var rigthSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Rigth);
if (leftSpecification is null)
return rigthSpecification;
if (rigthSpecification is null)
return leftSpecification;
ISpecification<TEntity>? result = null;
switch (vertex.Operation)
{
case OperationEnum.And:
result = new AndSpec<TEntity>(leftSpecification, rigthSpecification);
break;
case OperationEnum.Or:
result = new OrSpec<TEntity>(leftSpecification, rigthSpecification);
break;
}
return result;
}
private static ISpecification<TEntity>? LeafProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TLeaf leaf)
where TEntity : IValuesItem
{
var schemeProperty = dataSchemeDto.FirstOrDefault(e => e.PropertyName.Equals(leaf.PropName));
if (schemeProperty is null)
throw new ArgumentException($"Свойство {leaf.PropName} не найдено в схеме данных");
ISpecification<TEntity>? result = null;
switch (schemeProperty.PropertyKind)
{
case JsonValueKind.String:
var stringValue = Convert.ToString(leaf.Value);
var stringSpecifications = StringSpecifications<TEntity>();
result = stringSpecifications[leaf.Operation](schemeProperty.Index, stringValue);
break;
case JsonValueKind.Number:
var doubleValue = Convert.ToDouble(leaf.Value);
var doubleSpecifications = DoubleSpecifications<TEntity>();
result = doubleSpecifications[leaf.Operation](schemeProperty.Index, doubleValue);
break;
}
return result;
}
private static Dictionary<OperationEnum, Func<int, string?, ISpecification<TEntity>>> StringSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, string? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, string? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, string? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, string? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, string? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, string? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
private static Dictionary<OperationEnum, Func<int, double?, ISpecification<TEntity>>> DoubleSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, double? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, double? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, double? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, double? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, double? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, double? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
}

View File

@ -0,0 +1,20 @@
using System.Linq.Expressions;
namespace DD.Persistence.Database.Helpers;
public class ParameterReplacerVisitor : ExpressionVisitor
{
private readonly Expression _newExpression;
private readonly ParameterExpression _oldParameter;
private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression)
{
_oldParameter = oldParameter;
_newExpression = newExpression;
}
internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression)
=> new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression);
protected override Expression VisitParameter(ParameterExpression p)
=> p == _oldParameter ? _newExpression : p;
}

View File

@ -1,30 +1,16 @@
using Microsoft.EntityFrameworkCore;
using DD.Persistence.Models.Requests;
using DD.Persistence.Models.Common;
using DD.Persistence.ModelsAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Extensions;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository;
namespace DD.Persistence.Database.Postgres.Helpers;
/// <summary>
/// класс с набором методов, необходимых для фильтрации записей
/// </summary>
public static class QueryBuilders
{
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> query, DateTimeOffset momentUtc)
where TEntity : class, IChangeLog
{
momentUtc = momentUtc.ToUniversalTime();
query = query
.Where(e => e.Creation <= momentUtc)
.Where(e => e.Obsolete == null || e.Obsolete >= momentUtc);
return query;
}
public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query,
PaginationRequest request,
@ -55,4 +41,4 @@ public static class QueryBuilders
return result;
}
}
}

View File

@ -0,0 +1,28 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using Mapster;
namespace DD.Persistence.Database;
public static class MapsterSetup
{
public static void Configure()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogBaseDto()
{
Value = src.Value,
Id = src.Id
});
TypeAdapterConfig<KeyValuePair<Guid, SchemePropertyDto>, SchemeProperty>.NewConfig()
.Map(dest => dest.DiscriminatorId, src => src.Key)
.Map(dest => dest.Index, src => src.Value.Index)
.Map(dest => dest.PropertyKind, src => src.Value.PropertyKind)
.Map(dest => dest.PropertyName, src => src.Value.PropertyName);
}
}

View File

@ -10,12 +10,14 @@ public class PersistenceDbContext : DbContext
{
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<DataScheme> DataSchemes => Set<DataScheme>();
public DbSet<SchemeProperty> SchemeProperty => Set<SchemeProperty>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<ChangeLogCommit> ChangeLogCommit => Set<ChangeLogCommit>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>();
@ -30,10 +32,6 @@ public class PersistenceDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DataScheme>()
.Property(e => e.PropNames)
.HasJsonConversion();
modelBuilder.Entity<TimestampedValues>()
.Property(e => e.Values)
.HasJsonConversion();

View File

@ -0,0 +1,171 @@
using Ardalis.Specification;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogCommitRepository : IChangeLogCommitRepository
{
private DbContext db;
public ChangeLogCommitRepository(DbContext db)
{
this.db = db;
}
public async Task<ChangeLogCommitDto> Add(ChangeLogCommitCreateRequest request, CancellationToken token)
{
var commit = new ChangeLogCommit()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
DiscriminatorId = request.DiscriminatorId,
IdAuthor = request.IdAuthor,
Comment = request.Comment,
};
db.Set<ChangeLogCommit>().Add(commit);
await db.SaveChangesAsync(token);
var commitDto = commit.Adapt<ChangeLogCommitDto>();
return commitDto;
}
public async Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == request.DiscriminatorId);
if (request.UserId.HasValue)
query = query.Where(e => e.IdAuthor == request.UserId);
if(request.GeDate.HasValue)
query = query.Where(e => e.Creation >= request.GeDate);
if (request.LeDate.HasValue)
query = query.Where(e => e.Creation <= request.LeDate);
query = query
.Include(e => e.ChangeLogCreatedItems)
.Include(e => e.ChangeLogObsoletedItems);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(entity => new ChangeLogCommitDto()
{
ChangeLogCreatedItems = entity.ChangeLogCreatedItems.Select(c => c.Adapt<ChangeLogDto>()),
ChangeLogObsoletedItems = entity.ChangeLogObsoletedItems.Select(c => c.Adapt<ChangeLogDto>()),
Comment = entity.Comment,
Creation = entity.Creation,
DiscriminatorId = entity.DiscriminatorId,
IdAuthor = entity.IdAuthor,
Id = entity.Id,
});
return dtos;
}
public async Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(Guid discriminatorId, Guid? userId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId);
if (userId.HasValue)
query = query.Where(e => e.IdAuthor == userId);
var queryStat = query
.GroupBy(e => e.Creation.Date)
.Select(group => new
{
Date = group.Key,
CommitCount = group.Count(),
CreatedChangeLogCount = group.Sum(e => e.ChangeLogCreatedItems.Count()),
ObsoletedChangeLogCount = group.Sum(e => e.ChangeLogObsoletedItems.Count()),
});
var entities = await queryStat.ToListAsync(token);
var dtos = entities.Select(e => new ChangeLogStatisticsDto
{
CommitCount = e.CommitCount,
Date = new DateOnly(e.Date.Year, e.Date.Month, e.Date.Day),
CreatedChangeLogCount = e.CreatedChangeLogCount,
ObsoletedCount = e.ObsoletedChangeLogCount,
});
return dtos;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId)
.GroupBy(e => true)
.Select(group => new
{
Min = group.Min(e => e.Creation),
Max = group.Max(e => e.Creation)
});
var values = await query.FirstOrDefaultAsync(token);
if (values is null)
{
return null;
}
return new DatesRangeDto
{
From = values.Min,
To = values.Max,
};
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid discriminatorId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId)
.GroupBy(e => e.Creation.Date)
.Select(g => g.Key);
var entities = await query.ToArrayAsync(token);
var dates = entities
.Select(e => new DateOnly(e.Year, e.Month, e.Minute));
return dates;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var min = dateBegin.ToUniversalTime();
var max = dateEnd.ToUniversalTime();
var query = db.Set<ChangeLogCommit>()
.Where(c => c.DiscriminatorId == idDiscriminator)
.Where(c => c.Creation >= min && c.Creation <= max)
.Include(c => c.ChangeLogCreatedItems)
.Include(c => c.ChangeLogObsoletedItems);
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities
.SelectMany(c => new[] { c.ChangeLogCreatedItems, c.ChangeLogObsoletedItems })
.SelectMany(c => c)
.Where(c => c != null)
.Select(ConvertChangeLogToDto);
return dtos;
}
private ChangeLogDto ConvertChangeLogToDto(ChangeLog entity)
{
var dto = entity.Adapt<ChangeLogDto>();
dto.Creation = entity.CreatedCommit?.Creation;
dto.Obsolete = entity.ObsoletedCommit?.Creation;
dto.IdAuthor = entity.CreatedCommit?.IdAuthor;
dto.IdEditor = entity.ObsoletedCommit?.IdAuthor;
return dto;
}
}

View File

@ -0,0 +1,193 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(ChangeLogCommitDto commitDto, IEnumerable<IDictionary<string, object>> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
IdCreatedCommit = commitDto.Id,
Value = dto,
};
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<int> MarkAsDeleted(IEnumerable<Guid> ids, ChangeLogCommitDto commit, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
.Where(s => s.IdObsoletedCommit == null);
if (query.Count() != ids.Count())
{
throw new ArgumentException("Count of active items not equal count of ids", nameof(ids));
}
var entities = await query.ToArrayAsync(token);
foreach (var entity in entities)
{
entity.IdObsoletedCommit = commit.Id;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> MarkAsDeletedByDiscriminator(ChangeLogCommitDto commit, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.CreatedCommit.DiscriminatorId == commit.DiscriminatorId)
.Where(e => e.IdObsoletedCommit == null);
var entities = await query.ToArrayAsync(token);
foreach (var entity in entities)
{
entity.IdObsoletedCommit = commit.Id;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(ChangeLogCommitDto commit, IEnumerable<IDictionary<string, object>> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeletedByDiscriminator(commit, token);
result += await AddRange(commit, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogBaseDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
var updatedIds = dtos.Select(d => d.Id);
var updatedEntities = dbSet
.Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id);
using var transaction = await db.Database.BeginTransactionAsync(token);
foreach (var dto in dtos)
{
var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id);
if (updatedEntity is null)
{
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = new ChangeLog()
{
Id = Uuid7.Guid(),
Value = dto.Value,
IdCreatedCommit = commitDto.Id,
};
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.IdObsoletedCommit = commitDto.Id;
}
var result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
PaginationRequest paginationRequest,
CancellationToken token)
{
var momentUtc = moment.ToUniversalTime();
var query = db.Set<ChangeLog>()
.Where(s => s.CreatedCommit.DiscriminatorId == idDiscriminator)
.Where(e => e.CreatedCommit.Creation <= momentUtc)
.Where(e => e.ObsoletedCommit == null || e.ObsoletedCommit.Creation >= momentUtc)
.AsNoTracking();
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}
public async Task<IEnumerable<ChangeLogBaseDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = db.Set<ChangeLog>()
.Where(e => e.CreatedCommit.DiscriminatorId == idDiscriminator)
.Where(e => e.CreatedCommit.Creation >= date || (e.IdObsoletedCommit != null && e.ObsoletedCommit!.Creation >= date));
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities.Select(Convert);
return dtos;
}
public async Task<IEnumerable<ChangeLogDto>> GetByUserIdAndDiscriminatorId(ChangeLogQuery request, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Include(c => c.CreatedCommit)
.Include(c => c.ObsoletedCommit)
.Where(c => c.CreatedCommit.DiscriminatorId == request.DiscriminatorId)
.Where(c => c.CreatedCommit.IdAuthor == request.UserId
|| (c.ObsoletedCommit != null && c.ObsoletedCommit!.IdAuthor == request.UserId));
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities.Select(ConvertFull);
return dtos;
}
private ChangeLogBaseDto Convert(ChangeLog entity) => entity.Adapt<ChangeLogBaseDto>();
private ChangeLogDto ConvertFull(ChangeLog entity)
{
var dto = entity.Adapt<ChangeLogDto>();
dto.Creation = entity.CreatedCommit.Creation;
dto.Obsolete = entity.ObsoletedCommit?.Creation;
dto.IdAuthor = entity.CreatedCommit.IdAuthor;
dto.IdEditor = entity.ObsoletedCommit?.IdAuthor;
return dto;
}
}

View File

@ -4,7 +4,7 @@ using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class DataSourceSystemRepository : IDataSourceSystemRepository
{
protected DbContext db;
@ -12,7 +12,7 @@ public class DataSourceSystemRepository : IDataSourceSystemRepository
{
this.db = db;
}
protected virtual IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
protected IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token)
{

View File

@ -5,7 +5,7 @@ using DD.Persistence.Models;
using DD.Persistence.Repositories;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class ParameterRepository : IParameterRepository
{
private DbContext db;
@ -15,7 +15,7 @@ public class ParameterRepository : IParameterRepository
this.db = db;
}
protected virtual IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
protected IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{

View File

@ -0,0 +1,43 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Repositories;
public class SchemePropertyRepository : ISchemePropertyRepository
{
protected DbContext db;
public SchemePropertyRepository(DbContext db)
{
this.db = db;
}
protected IQueryable<SchemeProperty> GetQueryReadOnly() => db.Set<SchemeProperty>();
public virtual async Task AddRange(DataSchemeDto dataSchemeDto, CancellationToken token)
{
var entities = dataSchemeDto.Select(e =>
KeyValuePair.Create(dataSchemeDto.DiscriminatorId, e)
.Adapt<SchemeProperty>()
);
await db.Set<SchemeProperty>().AddRangeAsync(entities, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entities = await query.ToArrayAsync(token);
DataSchemeDto? result = null;
if (entities.Length != 0)
{
var properties = entities.Select(e => e.Adapt<SchemePropertyDto>()).ToArray();
result = new DataSchemeDto(dataSchemeId, properties);
}
return result;
}
}

View File

@ -6,7 +6,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace DD.Persistence.Repository.Repositories
namespace DD.Persistence.Database.Postgres.Repositories
{
public class SetpointRepository : ISetpointRepository
{
@ -16,11 +16,11 @@ namespace DD.Persistence.Repository.Repositories
this.db = db;
}
protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
protected IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(
IEnumerable<Guid> setpointKeys,
CancellationToken token)
IEnumerable<Guid> setpointKeys,
CancellationToken token)
{
var query = GetQueryReadOnly();

View File

@ -7,7 +7,7 @@ using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories
namespace DD.Persistence.Database.Postgres.Repositories
{
public class TechMessagesRepository : ITechMessagesRepository
{
@ -20,7 +20,7 @@ namespace DD.Persistence.Repository.Repositories
this.sourceSystemRepository = sourceSystemRepository;
}
protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
protected IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
.Include(e => e.System);
public async Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token)

View File

@ -1,22 +1,25 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
namespace DD.Persistence.Database.Postgres.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository
{
private readonly DbContext db;
public TimestampedValuesRepository(DbContext db)
private readonly ISchemePropertyRepository schemePropertyRepository;
public TimestampedValuesRepository(DbContext db, ISchemePropertyRepository schemePropertyRepository)
{
this.db = db;
this.schemePropertyRepository = schemePropertyRepository;
}
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
protected IQueryable<TimestampedValues> GetQueryReadOnly() => db.Set<TimestampedValues>();
public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var timestampedValuesEntities = dtos.Select(dto => new TimestampedValues()
{
@ -24,36 +27,47 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
Timestamp = dto.Timestamp.ToUniversalTime(),
Values = dto.Values.Values.ToArray()
});
await db.Set<TimestampedValues>().AddRangeAsync(timestampedValuesEntities, token);
await db.AddRangeAsync(timestampedValuesEntities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
public async virtual Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin,
public async Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? geTimestamp,
TNode? filterTree,
IEnumerable<string>? columnNames,
int skip,
int take,
CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(entity => discriminatorIds.Contains(entity.DiscriminatorId));
// Фильтрация по дате
if (timestampBegin.HasValue)
var resultQuery = Array.Empty<TimestampedValues>().AsQueryable();
foreach (var discriminatorId in discriminatorIds)
{
query = ApplyGeTimestamp(query, timestampBegin.Value);
var scheme = await schemePropertyRepository.Get(discriminatorId, token);
if (scheme == null)
throw new NotSupportedException($"Для переданного дискриминатора {discriminatorId} не была обнаружена схема данных");
var geTimestampUtc = geTimestamp!.Value.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId)
.Where(entity => entity.Timestamp >= geTimestampUtc);
if (filterTree != null)
query = query.ApplyFilter(scheme, filterTree);
resultQuery = resultQuery.Any() ? resultQuery.Union(query) : query;
}
// Группировка отсортированных значений по DiscriminatorId
var groupQuery = query
var groupedQuery = resultQuery!
.GroupBy(e => e.DiscriminatorId)
.Select(g => KeyValuePair.Create(g.Key, g.OrderBy(i => i.Timestamp).Skip(skip).Take(take)));
var entities = await groupQuery.ToArrayAsync(token);
.Select(g => KeyValuePair.Create(
g.Key,
g.OrderBy(i => i.Timestamp).Skip(skip).Take(take))
);
var entities = await groupedQuery.ToArrayAsync(token);
var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => (
e.Timestamp,
e.Values
@ -62,7 +76,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp)
@ -77,7 +91,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp)
@ -93,7 +107,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
}
// ToDo: прореживание должно осуществляться до материализации
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,
@ -114,10 +128,11 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset gtTimestamp, CancellationToken token)
{
var gtTimestampUtc = gtTimestamp.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(e => e.Timestamp > timestampBegin);
.Where(entity => entity.Timestamp > gtTimestampUtc);
var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => (
@ -128,7 +143,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.GroupBy(entity => entity.DiscriminatorId)
@ -153,26 +168,12 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return dto;
}
public virtual Task<int> Count(Guid discriminatorId, CancellationToken token)
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
{
var dbSet = db.Set<TimestampedValues>();
var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId);
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId);
return query.CountAsync(token);
}
/// <summary>
/// Применить фильтр по дате
/// </summary>
/// <param name="query"></param>
/// <param name="timestampBegin"></param>
/// <returns></returns>
private IQueryable<TimestampedValues> ApplyGeTimestamp(IQueryable<TimestampedValues> query, DateTimeOffset timestampBegin)
{
var geTimestampUtc = timestampBegin.ToUniversalTime();
var result = query
.Where(entity => entity.Timestamp >= geTimestampUtc);
var result = await query.CountAsync(token);
return result;
}

View File

@ -1,10 +1,10 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Database.Postgres.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DD.Persistence.Repository.RepositoriesCached;
namespace DD.Persistence.Database.Postgres.RepositoriesCached;
public class DataSourceSystemCachedRepository : DataSourceSystemRepository
{
private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey";

View File

@ -1,21 +1,21 @@
using DD.Persistence.Models;
using DD.Persistence.Repository.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Repositories;
namespace DD.Persistence.Repository.RepositoriesCached;
public class DataSchemeCachedRepository : DataSchemeRepository
namespace DD.Persistence.Database.RepositoriesCached;
public class SchemePropertyCachedRepository : SchemePropertyRepository
{
private readonly IMemoryCache memoryCache;
public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
public SchemePropertyCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{
this.memoryCache = memoryCache;
}
public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
public override async Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
await base.Add(dataSourceSystemDto, token);
await base.AddRange(dataSourceSystemDto, token);
memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto);
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
namespace DD.Persistence.Database.Specifications.Operation;
public class AndSpec<TEntity> : Specification<TEntity>
{
public AndSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
if (first is null || second is null)
return;
ApplyCriteria(first);
ApplyCriteria(second);
}
private void ApplyCriteria(ISpecification<TEntity> specification)
{
foreach (var criteria in specification.WhereExpressions)
{
Query.Where(criteria.Filter);
}
}
}

View File

@ -0,0 +1,15 @@
using Ardalis.Specification;
using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.Database.Specifications.Operation;
public class OrSpec<TEntity> : Specification<TEntity>
{
public OrSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
var orExpression = first.Or(second);
if (orExpression == null)
return;
Query.Where(orExpression);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация эквивалентности значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) == value);
}
public ValueEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) == value);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) >= 0);
}
public ValueGreateOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) >= value);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) > 0);
}
public ValueGreateSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) > value);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) <= 0);
}
public ValueLessOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) <= value);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) < 0);
}
public ValueLessSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) < value);
}
}

View File

@ -0,0 +1,22 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация неравенства значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueNotEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueNotEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) != value);
}
public ValueNotEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) != value);
}
}

View File

@ -4,7 +4,6 @@ using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
namespace DD.Persistence.IntegrationTests;
@ -40,12 +39,11 @@ public static class ApiTokenHelper
var nameIdetifier = Guid.NewGuid().ToString();
var claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, nameIdetifier),
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType),
new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
new(ClaimTypes.NameIdentifier.ToString(), "067beb95-3b6d-7370-8000-1b9b336a6cdc")
};
var tokenDescriptor = new SecurityTokenDescriptor

View File

@ -3,19 +3,22 @@ using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Requests;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using UuidExtensions;
using Xunit;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace DD.Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest
{
private readonly IChangeLogClient client;
private static readonly Random generatorRandomDigits = new();
private readonly PaginationRequest paginationRequest;
private static readonly Random random = new();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
{
@ -25,54 +28,36 @@ public class ChangeLogControllerTest : BaseIntegrationTest
client = scope.ServiceProvider
.GetRequiredService<IChangeLogClient>();
paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = string.Empty,
};
}
[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);
}
[Fact]
public async Task ClearAndInsertRange_InNotEmptyDb()
public async Task ClearAndInsertRange()
{
// arrange
var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var idDiscriminator = createdResult.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<ChangeLogValuesDto>());
var idDiscriminator = Guid.NewGuid();
var otherOldDtos = GenerateChangeLogCreateRequest(10);
await client.AddRange(Guid.NewGuid(), otherOldDtos, Guid.NewGuid().ToString(), CancellationToken.None);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
var oldDtos = GenerateChangeLogCreateRequest(10);
await client.AddRange(idDiscriminator, oldDtos, Guid.NewGuid().ToString(), CancellationToken.None);
var newDtos = GenerateChangeLogCreateRequest(insertedCount);
//act
var result = await client.ClearAndAddRange(idDiscriminator, newDtos, "Добавление новых элементов и очистка старых", 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);
var data = await client.GetByDate(idDiscriminator, DateTimeOffset.Now.AddSeconds(1), new PaginationRequest { Take = 10 * insertedCount }, CancellationToken.None);
Assert.Equal(insertedCount, data.Count);
}
[Fact]
@ -81,10 +66,11 @@ public class ChangeLogControllerTest : BaseIntegrationTest
// arrange
var count = 3;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var dtos = GenerateChangeLogCreateRequest(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,44 +79,42 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task Update_returns_success()
{
// arrange
dbContext.CleanupDbSet<ChangeLog>();
//arrange
var begin = DateTimeOffset.UtcNow;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
var dtos = GenerateChangeLogCreateRequest(1);
var original = dtos.First();
var comment = "Создаю 1 элемент";
var result = await client.AddRange(idDiscriminator, [original], comment, CancellationToken.None);
var entity = dbContext.ChangeLog
.Where(x => x.IdDiscriminator == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<ChangeLogValuesDto>();
var query = dbContext.Set<ChangeLog>()
.Include(x => x.CreatedCommit)
.Where(x => x.CreatedCommit.DiscriminatorId == idDiscriminator);
var entity = await query.FirstAsync();
var modified = entity.Adapt<ChangeLogBaseDto>();
var key = modified.Value.First().Key;
modified.Value[key] = random.NextDouble();
// act
result = await client.Update(dto, new CancellationToken());
comment = "Обновляю 1 элемент";
result = await client.UpdateRange(idDiscriminator, [modified], comment, CancellationToken.None);
// assert
Assert.Equal(2, result);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-1);
var dateEnd = DateTimeOffset.UtcNow.AddDays(1);
var now = DateTimeOffset.UtcNow.AddSeconds(1);
var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, new CancellationToken());
var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, begin, now, new CancellationToken());
Assert.NotNull(changeLogResult);
var obsoleteDto = changeLogResult
.Where(e => e.Obsolete.HasValue)
.FirstOrDefault();
.First();
var activeDto = changeLogResult
.Where(e => !e.Obsolete.HasValue)
.FirstOrDefault();
if (obsoleteDto == null || activeDto == null)
{
Assert.Fail();
return;
}
.OrderByDescending(e => e.Creation)
.First();
Assert.Equal(activeDto.Id, obsoleteDto.IdNext);
@ -139,193 +123,321 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task UpdateRange_returns_success()
{
// arrange
var count = 2;
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 idDiscriminator = Guid.NewGuid();
var dtos = GenerateChangeLogCreateRequest(count);
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(idDiscriminator, 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 idDiscriminator = Guid.NewGuid();
var otherOldDtos = GenerateChangeLogCreateRequest(10);
await client.AddRange(Guid.NewGuid(), otherOldDtos, Guid.NewGuid().ToString(), CancellationToken.None);
var oldDtos = GenerateChangeLogCreateRequest(10);
await client.AddRange(idDiscriminator, oldDtos, Guid.NewGuid().ToString(), CancellationToken.None);
var existing = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddSeconds(1), new PaginationRequest { Take= 100*insertedCount}, CancellationToken.None);
var ids = existing.Items.Select(e => e.Id);
// act
var ids = entities.Select(e => e.Id);
var result = await client.DeleteRange(ids, new CancellationToken());
var result = await client.DeleteRange(idDiscriminator, ids, "Удаление нескольких записей", CancellationToken.None);
// assert
Assert.Equal(count, result);
Assert.Equal(insertedCount, result);
var items = await dbContext.Set<ChangeLog>()
.Where(e=> ids.Contains(e.Id))
.ToArrayAsync(CancellationToken.None);
Assert.Equal(insertedCount, items.Count());
Assert.All(items, i => Assert.NotNull(i.IdObsoletedCommit));
}
static ChangeLogCommit GenerateCommit(Guid discriminatorId, DateTimeOffset creation)
=> GenerateCommit(discriminatorId, Guid.NewGuid(), creation);
static ChangeLogCommit GenerateCommit(Guid discriminatorId, Guid userId, DateTimeOffset creation)
=> new ChangeLogCommit
{
Id = Guid.NewGuid(),
DiscriminatorId = discriminatorId,
IdAuthor = userId,
Creation = creation,
Comment = Guid.NewGuid().ToString(),
};
static IEnumerable<ChangeLog> GenerateChangeLog(int count, ChangeLogCommit createCommit)
=> GenerateChangeLog(count, createCommit, null, null);
static IEnumerable<ChangeLog> GenerateChangeLog(int count, ChangeLogCommit createCommit, ChangeLogCommit? obsoleteCommit, Guid? idNext)
{
for (var i = 0; i< count; i++)
{
yield return new ChangeLog
{
Id = Guid.NewGuid(),
CreatedCommit = createCommit,
IdCreatedCommit = createCommit.Id,
ObsoletedCommit = obsoleteCommit,
IdObsoletedCommit = obsoleteCommit?.Id,
IdNext = idNext,
Value = new Dictionary<string, object> { { "Guid.NewGuid()", random.NextDouble() } }
};
}
}
static ChangeLogCommit GenerateCommitWithNewData(Guid discriminatorId, DateTimeOffset creation, int count)
{
var commit = GenerateCommit(discriminatorId, creation);
commit.ChangeLogCreatedItems = GenerateChangeLog(count, commit).ToList();
return commit;
}
async Task<ChangeLogCommit> SeedCommitWithNewData(Guid discriminatorId, DateTimeOffset creation, int count)
{
var data = GenerateCommitWithNewData(discriminatorId, creation, count);
dbContext.Set<ChangeLogCommit>().Add(data);
await dbContext.SaveChangesAsync();
return data;
}
[Fact]
public async Task GetDatesRange_returns_success()
{
// arrange
var changeLogItems = CreateChangeLogItems(3, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2.OrderBy(e => e.Creation);
//arrange
var dateMin = DateTimeOffset.UtcNow.AddDays(-1);
var dateMax = DateTimeOffset.UtcNow;
var dateMid = dateMin + 0.5 * (dateMax - dateMin);
var idDiscriminator = Guid.NewGuid();
var idOtherDiscriminator = Guid.NewGuid();
var count = 15;
await SeedCommitWithNewData(idDiscriminator, dateMin, count);
await SeedCommitWithNewData(idDiscriminator, dateMid, count);
await SeedCommitWithNewData(idDiscriminator, dateMax, count);
await SeedCommitWithNewData(idOtherDiscriminator, dateMin.AddDays(-1), count);
// act
var result = await client.GetDatesRange(idDiscriminator, new CancellationToken());
var result = await client.GetDatesRange(idDiscriminator, CancellationToken.None);
// assert
Assert.NotNull(result);
var minDate = entities.First().Creation;
var maxDate = entities.Last().Creation;
var expectedMinDate = minDate.ToUniversalTime().ToString();
var actualMinDate = result.From.ToUniversalTime().ToString();
Assert.Equal(expectedMinDate, actualMinDate);
var expectedMaxDate = maxDate.ToUniversalTime().ToString();
var actualMaxDate = result.To.ToUniversalTime().ToString();
Assert.Equal(expectedMaxDate, actualMaxDate);
Assert.NotNull(result);
Assert.Equal(dateMin, result.From, TimeSpan.FromSeconds(1));
Assert.Equal(dateMax, result.To, TimeSpan.FromSeconds(1));
}
[Fact]
public async Task GetByDate_returns_success()
{
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var dbSetCommit = dbContext.Set<ChangeLogCommit>();
var dbSet = dbContext.Set<ChangeLog>();
//создаем записи
var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
var idDiscriminator = Guid.NewGuid();
var date = DateTimeOffset.UtcNow;
var expected = 10;
var skip = 2;
var take = skip + 2;
var count = expected + take;
var commit = GenerateCommit(idDiscriminator, date);
dbSetCommit.Add(commit);
var commitData = GenerateChangeLog(count, commit);
dbSet.AddRange(commitData);
//удаляем все созданные записи за исключением первой и второй
//даты 2-х оставшихся записей должны вернуться в методе GetByDate
var ids = entities.Select(e => e.Id);
var idsToDelete = ids.Skip(2);
await dbContext.SaveChangesAsync(CancellationToken.None);
var dataWithIds = await dbSet
.Where(e=>e.CreatedCommit.DiscriminatorId == idDiscriminator)
.AsNoTracking()
.ToArrayAsync(CancellationToken.None);
var deletedCount = await client.DeleteRange(idsToDelete, new CancellationToken());
var itemsToDelete = dataWithIds.Skip(skip).Take(take);
var idsToDelete = itemsToDelete.Select(x => x.Id).ToArray();
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken());
// act
var deletedCount = await client.DeleteRange(idDiscriminator, idsToDelete, "Удаление нескольких записей", CancellationToken.None);
// assert
var moment = DateTimeOffset.UtcNow.AddSeconds(1);
var result = await client.GetByDate(idDiscriminator, moment, new() { Take = count *100}, new CancellationToken());
Assert.NotNull(result);
var restEntities = entities.Where(e => !idsToDelete.Contains(e.Id));
Assert.Equal(restEntities.Count(), result.Count);
var actualIds = restEntities.Select(e => e.Id);
var expectedIds = result.Items.Select(e => e.Id);
Assert.Equivalent(expectedIds, actualIds);
Assert.Equal(expected, result.Items.Count());
}
[Theory]
[InlineData(5, -15, 15, -20, 20, 10)]
[InlineData(5, -15, -10, -16, -9, 5)]
public async Task GetChangeLogForInterval_returns_success(
int insertedCount,
int daysBeforeNowChangeLog,
int daysAfterNowChangeLog,
int daysBeforeNowFilter,
int daysAfterNowFilter,
int changeLogCount)
[Fact]
public async Task GetChangeLogForInterval_returns_success()
{
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var dateMin = DateTimeOffset.UtcNow.AddDays(- 11);
var dateMax = DateTimeOffset.UtcNow;
var dateMid = dateMin + 0.5 * (dateMax - dateMin);
var idDiscriminator = Guid.NewGuid();
var idOtherDiscriminator = Guid.NewGuid();
var count = 17;
//создаем записи
var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
var commitMin = await SeedCommitWithNewData(idDiscriminator, dateMin, count);
var commitMid = await SeedCommitWithNewData(idDiscriminator, dateMid, count);
await SeedCommitWithNewData(idDiscriminator, dateMax, count);
await SeedCommitWithNewData(idOtherDiscriminator, dateMin.AddDays(-1), count);
var dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>()).ToArray();
await client.UpdateRange(dtos, new CancellationToken());
var dateBegin = commitMin.Creation;
var dateEnd = commitMid.Creation.AddSeconds(1);
var expectedCount = commitMin.ChangeLogCreatedItems.Count()
+ commitMid.ChangeLogCreatedItems.Count();
//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);
Assert.Equal(changeLogCount, result.Count());
Assert.Equal(expectedCount, result.Count());
}
[Fact]
public async Task GetStatistics_returns_success()
{
// arrange
var discriminatorId = Guid.NewGuid();
// создаем записи
var date0 = DateTimeOffset.UtcNow.AddDays(-17);
var day0 = new DateOnly(date0.Year, date0.Month, date0.Day);
var inserted0 = 17;
var commit0 = await SeedCommitWithNewData(discriminatorId, date0, inserted0);
private static IEnumerable<ChangeLogValuesDto> Generate(int count)
// создаем еще записи
var date1 = DateTimeOffset.UtcNow.AddDays(-7);
var day1 = new DateOnly(date1.Year, date1.Month, date1.Day);
var inserted1 = inserted0 + 5;
var commit1 = await SeedCommitWithNewData(discriminatorId, date1, inserted1);
// создаем еще записи с новым дискриминатором
await SeedCommitWithNewData(Guid.NewGuid(), date1, 17);
// обновим
var date2 = DateTimeOffset.UtcNow;
var day2 = new DateOnly(date2.Year, date2.Month, date2.Day);
var updated = commit0.ChangeLogCreatedItems.Select(i => new ChangeLogBaseDto { Id = i.Id, Value = i.Value });
await client.UpdateRange(commit0.DiscriminatorId, updated, Guid.NewGuid().ToString(), CancellationToken.None);
// удалим из коммита
var idToDelete = commit1.ChangeLogCreatedItems.First().Id;
await client.DeleteRange(commit1.DiscriminatorId, [idToDelete], Guid.NewGuid().ToString(), CancellationToken.None);
//act
var request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null
};
var statistics = await client.GetStatistics(request, CancellationToken.None);
//assert
Assert.Equal(3, statistics.Count());
var stat0 = statistics.First(s => s.Date == day0);
Assert.Equal(inserted0, stat0.CreatedChangeLogCount);
Assert.Equal(0, stat0.ObsoletedCount);
Assert.Equal(1, stat0.CommitCount);
var stat1 = statistics.First(s => s.Date == day1);
Assert.Equal(inserted1, stat1.CreatedChangeLogCount);
Assert.Equal(0, stat1.ObsoletedCount);
Assert.Equal(1, stat1.CommitCount);
var stat2 = statistics.First(s => s.Date == day2);
Assert.Equal(inserted0, stat2.CreatedChangeLogCount);
Assert.Equal(inserted0 + 1, stat2.ObsoletedCount);
Assert.Equal(2, stat2.CommitCount);
}
[Fact]
public async Task GetHistory_returns_success()
{
// arrange
var discriminatorId = Guid.NewGuid();
// создаем записи
var date0 = DateTimeOffset.UtcNow.AddDays(-17);
var inserted0 = 17;
var commit0 = await SeedCommitWithNewData(discriminatorId, date0, inserted0);
// обновим
var date2 = DateTimeOffset.UtcNow;
var updated = commit0.ChangeLogCreatedItems.Skip(1).Select(i => new ChangeLogBaseDto { Id = i.Id, Value = i.Value });
await client.UpdateRange(commit0.DiscriminatorId, updated, Guid.NewGuid().ToString(), CancellationToken.None);
// удалим из коммита
var idToDelete = commit0.ChangeLogCreatedItems.First().Id;
await client.DeleteRange(commit0.DiscriminatorId, [idToDelete], Guid.NewGuid().ToString(), CancellationToken.None);
var request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null
};
var history = await client.GetHistory(request, CancellationToken.None);
Assert.Equal(3, history.Count());
var items = history.OrderBy(e => e.Creation);
var firstItem = items.First();
Assert.Equal(date0.DateTime.ToString(), firstItem.Creation.DateTime.ToString());
Assert.Equal(inserted0, firstItem.ChangeLogCreatedItems.Count());
Assert.Empty(firstItem.ChangeLogObsoletedItems);
var middleItem = items.Skip(1).Take(1).First();
Assert.Equal(date2.DateTime.ToString(), middleItem.Creation.DateTime.ToString());
Assert.Equal(updated.Count(), middleItem.ChangeLogObsoletedItems.Count());
Assert.Equal(updated.Count(), middleItem.ChangeLogCreatedItems.Count());
var lastItem = items.Last();
Assert.Equal(date2.DateTime.ToString(), lastItem.Creation.DateTime.ToString());
Assert.Single(lastItem.ChangeLogObsoletedItems);
//тест случая, когда данных за указанный период нет
request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null,
GeDate = DateTimeOffset.UtcNow.AddMinutes(1),
LeDate = DateTimeOffset.UtcNow.AddDays(10),
};
history = await client.GetHistory(request, CancellationToken.None);
Assert.Empty(history);
//тест случая, когда данные за указанный период есть
request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null,
GeDate = DateTimeOffset.UtcNow.AddDays(-17).AddMinutes(-10),
LeDate = DateTimeOffset.UtcNow.AddDays(-1),
};
history = await client.GetHistory(request, CancellationToken.None);
Assert.Single(history);
var createdChangeLogs = history.First().ChangeLogCreatedItems;
Assert.Equal(17, createdChangeLogs.Count());
}
private static IEnumerable<IDictionary<string, object>> GenerateChangeLogCreateRequest(int count)
{
for (int i = 0; i < count; i++)
yield return new ChangeLogValuesDto()
{
Value = new Dictionary<string, object>()
yield return new Dictionary<string, object>()
{
{ "Key", 1 }
},
Id = Guid.NewGuid()
};
{ "Key", random.NextDouble() }
};
}
private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange)
{
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.IdDiscriminator = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
return entity;
}).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
return (idDiscriminator, entities);
}
}

View File

@ -1,10 +1,10 @@
using DD.Persistence.Client;
using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Xunit;
@ -13,17 +13,14 @@ namespace DD.Persistence.IntegrationTests.Controllers
public class SetpointControllerTest : BaseIntegrationTest
{
private readonly ISetpointClient setpointClient;
private readonly SetpointConfigStorage configStorage;
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var refitClientFactory = scope.ServiceProvider
.GetRequiredService<IRefitClientFactory<IRefitSetpointClient>>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<SetpointClient>>();
setpointClient = scope.ServiceProvider
.GetRequiredService<ISetpointClient>();
configStorage = (SetpointConfigStorage)scope.ServiceProvider.GetRequiredService<ISetpointConfigStorage>();
}
@ -32,12 +29,16 @@ namespace DD.Persistence.IntegrationTests.Controllers
{
var id = Guid.Parse("e0fcad22-1761-476e-a729-a3c59d51ba41");
configStorage.AddOrReplace(id, typeof(float));
var config = new MappingConfig();
config[id] = typeof(float);
var setpointMapper = new SetpointMappingClient(setpointClient, config);
await setpointClient.Add(id, 48.3f, CancellationToken.None);
//act
var response = await setpointClient.GetCurrent([id], CancellationToken.None);
var response = await setpointMapper.GetCurrent([id], CancellationToken.None);
//assert
Assert.NotNull(response);

View File

@ -3,7 +3,6 @@ using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Database.Entity;
using DD.Persistence.Extensions;
using DD.Persistence.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
@ -39,7 +38,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
}
[Fact]
public async Task Get_returns_success()
public async Task Get_returns_BadRequest()
{
//arrange
Cleanup();
@ -50,11 +49,18 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var secondDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(secondDiscriminatorId);
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, 0, 1, CancellationToken.None);
try
{
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, null, 0, 1, CancellationToken.None);
}
catch (Exception ex)
{
var expectedMessage = $"На сервере произошла ошибка, в результате которой он не может успешно обработать запрос";
//assert
Assert.Null(response);
//assert
Assert.Equal(expectedMessage, ex.Message);
}
}
[Fact]
@ -71,22 +77,25 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
var columnNames = new List<string>() { "A", "C" };
var skip = 2;
var take = 16;
var skip = 0;
var take = 6; // Ровно столько значений будет удовлетворять фильтру (\"A\">3) (для одного дискриминатора)
var customFilter = "(\"A\">3)";
var dtos = (await AddRange(firstDiscriminatorId)).ToList();
dtos.AddRange(await AddRange(secondDiscriminatorId));
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId],
timestampBegin, columnNames, skip, take, CancellationToken.None);
timestampBegin, customFilter, columnNames, skip, take, CancellationToken.None);
//assert
Assert.NotNull(response);
Assert.NotEmpty(response);
var expectedCount = take * 2;
var actualCount = response.Count();
Assert.Equal(take, actualCount);
Assert.Equal(expectedCount, actualCount);
var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList();
Assert.Equal(columnNames, actualColumnNames);
@ -267,7 +276,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var count = 2048;
var timestampBegin = DateTimeOffset.UtcNow;
var dtos = await AddRange(discriminatorId, count);
//act
var response = await timestampedValuesClient.GetResampledData(discriminatorId, timestampBegin, count);
@ -379,7 +388,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var response = await timestampedValuesClient.AddRange(discriminatorId, generatedDtos, CancellationToken.None);
// assert
Assert.Equal(generatedDtos.Count(), response);
//Assert.Equal(generatedDtos.Count(), response);
return generatedDtos;
}
@ -407,8 +416,12 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup()
{
foreach (var item in discriminatorIds)
{
memoryCache.Remove(item);
}
discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<DataScheme>();
dbContext.CleanupDbSet<SchemeProperty>();
}
}

View File

@ -1,5 +1,4 @@
using DD.Persistence.Client.Helpers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
namespace DD.Persistence.IntegrationTests
{

View File

@ -5,13 +5,9 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using DD.Persistence.API;
using DD.Persistence.Client;
using DD.Persistence.Database.Model;
using DD.Persistence.App;
using DD.Persistence.Client.Helpers;
using DD.Persistence.Factories;
using System.Net;
using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.IntegrationTests;
@ -24,10 +20,9 @@ public class WebAppFactoryFixture : WebApplicationFactory<Program>
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("appsettings.Tests.json");
var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
var configurationRoot = config.Build();
var dbConnection = configurationRoot.GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
//connectionString = "Host=postgres;Port=5442;Username=postgres;Password=postgres;Database=persistence";
});
builder.ConfigureServices(services =>

View File

@ -1,9 +1,9 @@
namespace DD.Persistence.Models;
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// Dto для хранения записей
/// </summary>
public class ChangeLogValuesDto
public class ChangeLogBaseDto
{
/// <summary>
/// Ключ записи

View File

@ -0,0 +1,44 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель для создания коммита
/// </summary>
public class ChangeLogCommitCreateRequest
{
/// <summary>
/// DiscriminatorId
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Пользователь, совершающий коммит
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Комментарий
/// </summary>
public string Comment { get; set; } = null!;
/// <summary>
/// ctor
/// </summary>
public ChangeLogCommitCreateRequest()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="idAuthor"></param>
/// <param name="comment"></param>
public ChangeLogCommitCreateRequest(Guid discriminatorId, Guid idAuthor, string comment)
{
DiscriminatorId = discriminatorId;
IdAuthor = idAuthor;
Comment = comment;
}
}

View File

@ -0,0 +1,27 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель коммита с изменениями
/// </summary>
public class ChangeLogCommitDto : ChangeLogCommitCreateRequest
{
/// <summary>
/// Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Список созданных записей коммита
/// </summary>
public IEnumerable<ChangeLogDto> ChangeLogCreatedItems { get; set; } = null!;
/// <summary>
/// Список устаревших записей коммита
/// </summary>
public IEnumerable<ChangeLogDto> ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -1,19 +1,14 @@
namespace DD.Persistence.Models;
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public class ChangeLogDto
public class ChangeLogDto: ChangeLogBaseDto
{
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Создатель записи
/// </summary>
public Guid IdAuthor { get; set; }
public Guid? IdAuthor { get; set; }
/// <summary>
/// Пользователь, изменивший запись
@ -23,7 +18,7 @@ public class ChangeLogDto
/// <summary>
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
public DateTimeOffset? Creation { get; set; }
/// <summary>
/// Дата устаревания
@ -34,9 +29,4 @@ public class ChangeLogDto
/// Ключ заменившей записи
/// </summary>
public Guid? IdNext { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public ChangeLogValuesDto Value { get; set; } = default!;
}

View File

@ -0,0 +1,28 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Запрос, используемый для получения данных по журналу операций
/// </summary>
public class ChangeLogQuery
{
/// <summary>
/// Дискриминатор задачи
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Пользователь
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Больше или равно указанной даты
/// </summary>
public DateTimeOffset? GeDate { get; set; }
/// <summary>
/// Меньше или равно указанной даты
/// </summary>
public DateTimeOffset? LeDate { get; set; }
}

View File

@ -0,0 +1,27 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель, необходимая для отображения статистики по журналу изменений
/// </summary>
public class ChangeLogStatisticsDto
{
/// <summary>
/// Дата изменений
/// </summary>
public DateOnly Date { get; set; }
/// <summary>
/// Количество созданных элементов журнала изменений
/// </summary>
public int CreatedChangeLogCount { get; set; }
/// <summary>
/// Количество устаревших элементов журнала изменений
/// </summary>
public int ObsoletedCount { get; set; }
/// <summary>
/// Количество коммитов
/// </summary>
public int CommitCount { get; set; }
}

View File

@ -0,0 +1,4 @@
namespace DD.Persistence.Models.Configurations;
public class MappingConfig : Dictionary<Guid, Type>
{
}

View File

@ -1,9 +1,11 @@
namespace DD.Persistence.Models;
using System.Collections;
namespace DD.Persistence.Models;
/// <summary>
/// Схема для набора данных
/// </summary>
public class DataSchemeDto
public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumerable<SchemePropertyDto>>
{
/// <summary>
/// Дискриминатор
@ -11,7 +13,30 @@ public class DataSchemeDto
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Наименования полей
/// Поля
/// </summary>
public string[] PropNames { get; set; } = [];
private IEnumerable<SchemePropertyDto> Properties { get; } = [];
/// <inheritdoc/>
public DataSchemeDto(Guid discriminatorId, IEnumerable<SchemePropertyDto> Properties)
{
DiscriminatorId = discriminatorId;
this.Properties = Properties;
}
/// <inheritdoc/>
public IEnumerator<SchemePropertyDto> GetEnumerator()
=> Properties.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public bool Equals(IEnumerable<SchemePropertyDto>? otherProperties)
{
if (otherProperties is null)
return false;
return Properties.SequenceEqual(otherProperties);
}
}

View File

@ -0,0 +1,30 @@
using System.Text.Json;
namespace DD.Persistence.Models;
/// <summary>
/// Индексируемого поле из схемы для набора данных
/// </summary>
public class SchemePropertyDto : IEquatable<SchemePropertyDto>
{
/// <summary>
/// Индекс поля
/// </summary>
public required int Index { get; set; }
/// <summary>
/// Наименование индексируемого поля
/// </summary>
public required string PropertyName { get; set; }
/// <summary>
/// Тип индексируемого поля
/// </summary>
public required JsonValueKind PropertyKind { get; set; }
/// <inheritdoc/>
public bool Equals(SchemePropertyDto? other)
{
return Index == other?.Index && PropertyName == other?.PropertyName && PropertyKind == other?.PropertyKind;
}
}

View File

@ -7,6 +7,11 @@ namespace DD.Persistence.Models;
/// </summary>
public class TimestampedValuesDto : ITimestampAbstractDto
{
/// <summary>
/// Дискриминатор
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Временная отметка
/// </summary>

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Models;
/// <summary>
/// Класс, описывающий пользователя
/// </summary>
public class UserDto
{
/// <summary>
/// Идентификатор пользователя
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Имя пользователя для отображения
/// </summary>
public required string DisplayName { get; set; }
}

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" />
@ -20,7 +20,6 @@
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -1,12 +1,7 @@
using DD.Persistence.Database.Model;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Database.Postgres.Repositories;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace DD.Persistence.Repository.Test;
public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
@ -29,7 +24,6 @@ public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
var value = GetJsonFromObject(22);
await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None);
var t = fixture.dbContainer.GetConnectionString();
//act
var result = await sut.GetCurrent([id], CancellationToken.None);

View File

@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -1,255 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
{
var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto);
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
.Where(s => s.Obsolete == null);
if (query.Count() != ids.Count())
{
throw new ArgumentException("Count of active items not equal count of ids", nameof(ids));
}
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.IdDiscriminator == idDiscriminator)
.Where(e => e.Obsolete == null);
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;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeleted(idAuthor, idDiscriminator, token);
result += await AddRange(idAuthor, idDiscriminator, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
var updatedIds = dtos.Select(d => d.Id);
var updatedEntities = dbSet
.Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id);
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
foreach (var dto in dtos)
{
var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id);
if (updatedEntity is null)
{
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto);
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.Obsolete = DateTimeOffset.UtcNow;
updatedEntity.IdEditor = idEditor;
}
result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
PaginationRequest paginationRequest,
CancellationToken token)
{
var query = CreateQuery(idDiscriminator);
query = query.Apply(momentUtc);
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}
private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
return query;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(s => s.IdDiscriminator == idDiscriminator);
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);
var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);
query = createdQuery.Union(editedQuery);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<ChangeLogDto>());
return dtos;
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
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);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct()
.OrderBy(d => d);
return datesOnly;
}
private static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, ChangeLogValuesDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
IdDiscriminator = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
};
return entity;
}
public async Task<IEnumerable<ChangeLogValuesDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = this.db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(Convert);
return dtos;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.GroupBy(e => 1)
.Select(group => new
{
Min = group.Min(e => e.Creation),
Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation)
? e.Obsolete.Value
: e.Creation),
});
var values = await query.FirstOrDefaultAsync(token);
if (values is null)
{
return null;
}
return new DatesRangeDto
{
From = values.Min,
To = values.Max,
};
}
private ChangeLogValuesDto Convert(ChangeLog entity) => entity.Adapt<ChangeLogValuesDto>();
}

View File

@ -1,34 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class DataSchemeRepository : IDataSchemeRepository
{
protected DbContext db;
public DataSchemeRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<DataScheme> GetQueryReadOnly() => db.Set<DataScheme>();
public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataScheme>();
await db.Set<DataScheme>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entity = await query.ToArrayAsync();
var dto = entity.Select(e => e.Adapt<DataSchemeDto>()).FirstOrDefault();
return dto;
}
}

View File

@ -1,103 +0,0 @@
//using DD.Persistence.Models;
//using DD.Persistence.Models.Common;
//using DD.Persistence.Repositories;
//using Microsoft.EntityFrameworkCore;
//namespace DD.Persistence.Repository.Repositories;
//public class TimestampedValuesCachedRepository : TimestampedValuesRepository
//{
// public static TimestampedValuesDto? FirstByDate { get; private set; }
// public static CyclicArray<TimestampedValuesDto> LastData { get; } = new CyclicArray<TimestampedValuesDto>(CacheItemsCount);
// private const int CacheItemsCount = 3600;
// public TimestampedValuesCachedRepository(DbContext db, IDataSourceSystemRepository<ValuesIdentityDto> relatedDataRepository) : base(db, relatedDataRepository)
// {
// //Task.Run(async () =>
// //{
// // var firstDateItem = await base.GetFirst(CancellationToken.None);
// // if (firstDateItem == null)
// // {
// // return;
// // }
// // FirstByDate = firstDateItem;
// // var dtos = await base.GetLast(CacheItemsCount, CancellationToken.None);
// // dtos = dtos.OrderBy(d => d.Timestamp);
// // LastData.AddRange(dtos);
// //}).Wait();
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token)
// {
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// var dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// return dtos;
// }
// var items = LastData
// .Where(i => i.Timestamp >= dateBegin);
// return items;
// }
// public override async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
// {
// var result = await base.AddRange(discriminatorId, dtos, token);
// if (result > 0)
// {
// dtos = dtos.OrderBy(x => x.Timestamp);
// FirstByDate = dtos.First();
// LastData.AddRange(dtos);
// }
// return result;
// }
// public override async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
// {
// if (FirstByDate == null)
// return null;
// return await Task.Run(() =>
// {
// return new DatesRangeDto
// {
// From = FirstByDate.Timestamp,
// To = LastData[^1].Timestamp
// };
// });
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetResampledData(
// Guid discriminatorId,
// DateTimeOffset dateBegin,
// double intervalSec = 600d,
// int approxPointsCount = 1024,
// CancellationToken token = default)
// {
// var dtos = LastData.Where(i => i.Timestamp >= dateBegin);
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// }
// var dateEnd = dateBegin.AddSeconds(intervalSec);
// dtos = dtos
// .Where(i => i.Timestamp <= dateEnd);
// var ratio = dtos.Count() / approxPointsCount;
// if (ratio > 1)
// dtos = dtos
// .Where((_, index) => index % ratio == 0);
// return dtos;
// }
//}

View File

@ -0,0 +1,305 @@
using DD.Persistence.Database.Repositories;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using DD.Persistence.Services;
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
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Добавление нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogRequests(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.AddRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<IDictionary<string, object>>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var addRangeResult = await service
.AddRange(commitRequest, dtos, CancellationToken.None);
addRangeResult = await service
.AddRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).AddRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task UpdateRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Изменение нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.UpdateRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogBaseDto>>(),
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 commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Удаление нескольких значений");
var commit = new ChangeLogCommitDto()
{
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.MarkAsDeleted(
Arg.Any<IEnumerable<Guid>>(),
Arg.Any<ChangeLogCommitDto>(),
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(dtoIds, commit, CancellationToken.None);
}
[Fact]
public async Task ClearAndAddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Удаление и добавление нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogRequests(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.ClearAndAddRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<IDictionary<string, object>>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var clearAndAddResult = await service
.ClearAndAddRange(commitRequest, dtos, CancellationToken.None);
clearAndAddResult = await service
.ClearAndAddRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).ClearAndAddRange(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<ChangeLogBaseDto>()
{
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;
changeLogCommitRepository
.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 changeLogCommitRepository.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 };
changeLogCommitRepository
.GetDatesChange(
Arg.Any<Guid>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetDatesChange(discriminatorId, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).GetDatesChange(discriminatorId, CancellationToken.None);
}
private IEnumerable<ChangeLogBaseDto> GenerateChangeLogValuesDto(int count)
{
var items = new List<ChangeLogBaseDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogBaseDto()
{
Id = Uuid7.Guid(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
private IEnumerable<IDictionary<string, object>> GenerateChangeLogRequests(int count)
{
var items = new List<IDictionary<string, object>>();
for (int i = 0; i < count; i++)
{
items.Add(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(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@ -16,8 +16,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
<ProjectReference Include="..\DD.Persistence.API\DD.Persistence.API.csproj" />
<ProjectReference Include="..\DD.Persistence.Client\DD.Persistence.Client.csproj" />
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,278 @@
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Helpers;
using System.Text.Json;
namespace DD.Persistence.Test;
/// ToDo: переписать под Theory
public class FilterBuilderShould
{
private readonly SpecificationEvaluator SpecificationEvaluator;
public FilterBuilderShould()
{
this.SpecificationEvaluator = new SpecificationEvaluator();
}
[Fact]
public void TestFilterBuilding()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var filterDate = DateTime.Now.AddMinutes(-1);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Greate, "A", filterDate),
new TLeaf(OperationEnum.Less, "B", 2.22)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 2;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterOperations()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Less, "A", 2),
new TLeaf(OperationEnum.LessOrEqual, "A", 1.99)
),
new TLeaf(OperationEnum.GreateOrEqual, "A", 1.97)
),
new TLeaf(OperationEnum.Greate, "A", 1.96)
),
new TLeaf(OperationEnum.NotEqual, "A", 1.98)
),
new TLeaf(OperationEnum.Equal, "A", 1)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 1.96 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 1.97 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 1.98 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-5),
Values = new object[] { 1.99 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-6),
Values = new object[] { 2 } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 3;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterValues()
{
//arrange
var discriminatorId = Guid.NewGuid();
var filterDate = DateTimeOffset.Now;
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 3,
PropertyName = "D",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1),
new TLeaf(OperationEnum.Equal, "B", 1.11)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
),
new TLeaf(OperationEnum.Equal, "D", filterDate)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 4;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
}

View File

@ -0,0 +1,99 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.Logging;
using NSubstitute;
using System.Text.Json;
namespace DD.Persistence.Test;
public record FirstTestDto(Guid DiscriminatorId, DateTimeOffset Timestamp, int Id, string? Value);
public record SecondTestDto(Guid DiscriminatorId, DateTimeOffset Timestamp, int Id, double Capacity);
public class MappingClientsTest
{
private readonly ITimestampedValuesClient timestampedValuesClient = Substitute.For<ITimestampedValuesClient>();
private readonly ILogger<TimestampedSetMapper> logger = Substitute.For<ILogger<TimestampedSetMapper>>();
private readonly TimestampedMappingClient timestampedMappingClient;
private readonly MappingConfig mappingConfigs;
public MappingClientsTest()
{
mappingConfigs = GetConfig();
var storage = new MapperStorage(mappingConfigs, logger);
timestampedMappingClient = new TimestampedMappingClient(timestampedValuesClient, storage);
}
[Fact]
public async Task GetMultiMapped()
{
// Arrange
var discriminatorIds = mappingConfigs.Keys;
var firstDiscriminatorId = discriminatorIds.First();
var secondDiscriminatorId = discriminatorIds.Last();
var getResult = new[]
{
new TimestampedValuesDto()
{
DiscriminatorId = firstDiscriminatorId,
Timestamp = DateTime.UtcNow,
Values = new Dictionary<string, object>
{
{ nameof(FirstTestDto.Id), JsonDocument.Parse(JsonSerializer.Serialize(1)).RootElement },
{ nameof(FirstTestDto.Value), JsonDocument.Parse(JsonSerializer.Serialize("string1")).RootElement}
}
},
new TimestampedValuesDto()
{
DiscriminatorId = secondDiscriminatorId,
Timestamp = DateTime.UtcNow,
Values = new Dictionary<string, object>
{
{ nameof(SecondTestDto.Id), JsonDocument.Parse(JsonSerializer.Serialize(1)).RootElement },
{ nameof(SecondTestDto.Capacity), JsonDocument.Parse(JsonSerializer.Serialize(0.1)).RootElement}
}
}
};
timestampedValuesClient
.Get(discriminatorIds, null, null, null, 0, 1, CancellationToken.None)
.ReturnsForAnyArgs(getResult);
// Act
var result = await timestampedMappingClient.GetMultiMapped(discriminatorIds, null, null, null, 0, 1, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(getResult.Count(), result.Count());
var firstActualDto = (FirstTestDto) result[firstDiscriminatorId].First();
Assert.NotNull(firstActualDto);
var actualId = firstActualDto.Id.ToString();
var expectedId = getResult[0].Values[nameof(FirstTestDto.Id)].ToString();
Assert.Equal(expectedId, actualId);
var secondActualDto = (SecondTestDto) result[secondDiscriminatorId].First();
Assert.NotNull(secondActualDto);
actualId = secondActualDto.Id.ToString();
expectedId = getResult[1].Values[nameof(SecondTestDto.Id)].ToString();
Assert.Equal(expectedId, actualId);
}
private MappingConfig GetConfig()
{
var config = new MappingConfig();
config[Guid.NewGuid()] = typeof(FirstTestDto);
config[Guid.NewGuid()] = typeof(SecondTestDto);
return config;
}
}

Some files were not shown because too many files have changed in this diff Show More