diff --git a/DD.Persistence.API/Controllers/ChangeLogController.cs b/DD.Persistence.API/Controllers/ChangeLogController.cs index 21761e1..8b788c4 100644 --- a/DD.Persistence.API/Controllers/ChangeLogController.cs +++ b/DD.Persistence.API/Controllers/ChangeLogController.cs @@ -4,6 +4,7 @@ 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; diff --git a/DD.Persistence.API/Controllers/DataSaubController.cs b/DD.Persistence.API/Controllers/DataSaubController.cs deleted file mode 100644 index 832faec..0000000 --- a/DD.Persistence.API/Controllers/DataSaubController.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; -using DD.Persistence.Repositories; - -namespace DD.Persistence.API.Controllers; - -/// -/// Работа с временными данными -/// -[ApiController] -[Authorize] -[Route("api/[controller]")] -public class DataSaubController : TimeSeriesController -{ - public DataSaubController(ITimeSeriesDataRepository timeSeriesDataRepository) : base(timeSeriesDataRepository) - { - - } -} diff --git a/DD.Persistence.API/Controllers/SetpointController.cs b/DD.Persistence.API/Controllers/SetpointController.cs index e3b8b14..2f1c6fc 100644 --- a/DD.Persistence.API/Controllers/SetpointController.cs +++ b/DD.Persistence.API/Controllers/SetpointController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc; using DD.Persistence.Models; using DD.Persistence.Repositories; using System.Net; +using System.Text.Json; +using DD.Persistence.Models.Common; namespace DD.Persistence.API.Controllers; @@ -28,9 +30,9 @@ public class SetpointController : ControllerBase, ISetpointApi /// /// [HttpGet("current")] - public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) + public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) { - var result = await setpointRepository.GetCurrent(setpointKeys, token); + var result = await setpointRepository.GetCurrentDictionary(setpointKeys, token); return Ok(result); } @@ -104,7 +106,7 @@ public class SetpointController : ControllerBase, ISetpointApi public async Task Add(Guid setpointKey, object newValue, CancellationToken token) { var userId = User.GetUserId(); - await setpointRepository.Add(setpointKey, newValue, userId, token); + await setpointRepository.Add(setpointKey, (JsonElement)newValue, userId, token); return CreatedAtAction(nameof(Add), true); } diff --git a/DD.Persistence.API/Controllers/TechMessagesController.cs b/DD.Persistence.API/Controllers/TechMessagesController.cs index 81663e0..cd98e42 100644 --- a/DD.Persistence.API/Controllers/TechMessagesController.cs +++ b/DD.Persistence.API/Controllers/TechMessagesController.cs @@ -4,6 +4,7 @@ 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; diff --git a/DD.Persistence.API/Controllers/TimeSeriesController.cs b/DD.Persistence.API/Controllers/TimeSeriesController.cs deleted file mode 100644 index 5fded36..0000000 --- a/DD.Persistence.API/Controllers/TimeSeriesController.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; -using DD.Persistence.Repositories; - -namespace DD.Persistence.API.Controllers; - -[ApiController] -[Authorize] -[Route("api/[controller]")] -public class TimeSeriesController : ControllerBase, ITimeSeriesDataApi - where TDto : class, ITimeSeriesAbstractDto, new() -{ - private readonly ITimeSeriesDataRepository timeSeriesDataRepository; - - public TimeSeriesController(ITimeSeriesDataRepository timeSeriesDataRepository) - { - this.timeSeriesDataRepository = timeSeriesDataRepository; - } - - /// - /// Получить список объектов, удовлетворяющий диапазону дат - /// - /// - /// - /// - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task Get(DateTimeOffset dateBegin, CancellationToken token) - { - var result = await timeSeriesDataRepository.GetGtDate(dateBegin, token); - return Ok(result); - } - - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - [HttpGet("datesRange")] - public async Task GetDatesRange(CancellationToken token) - { - var result = await timeSeriesDataRepository.GetDatesRange(token); - return Ok(result); - } - - /// - /// Получить список объектов с прореживанием, удовлетворяющий диапазону дат - /// - /// - /// - /// - /// - /// - [HttpGet("resampled")] - public async Task GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default) - { - var result = await timeSeriesDataRepository.GetResampledData(dateBegin, intervalSec, approxPointsCount, token); - return Ok(result); - } - - /// - /// Добавить записи - /// - /// - /// - /// - [HttpPost] - public async Task AddRange(IEnumerable dtos, CancellationToken token) - { - var result = await timeSeriesDataRepository.AddRange(dtos, token); - return Ok(result); - } - - -} diff --git a/DD.Persistence.API/Controllers/TimestampedSetController.cs b/DD.Persistence.API/Controllers/TimestampedSetController.cs deleted file mode 100644 index 0c805ab..0000000 --- a/DD.Persistence.API/Controllers/TimestampedSetController.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; -using DD.Persistence.Repositories; -using System.Net; - -namespace DD.Persistence.API.Controllers; - -/// -/// Хранение наборов данных с отметкой времени. -/// Не оптимизировано под большие данные. -/// -[ApiController] -[Authorize] -[Route("api/[controller]/{idDiscriminator}")] -public class TimestampedSetController : ControllerBase -{ - private readonly ITimestampedSetRepository repository; - - public TimestampedSetController(ITimestampedSetRepository repository) - { - this.repository = repository; - } - - /// - /// Записать новые данные - /// Предполагается что данные с одним дискриминатором имеют одинаковую структуру - /// - /// Дискриминатор (идентификатор) набора - /// - /// - /// кол-во затронутых записей - [HttpPost] - [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] - public async Task AddRange([FromRoute] Guid idDiscriminator, [FromBody] IEnumerable sets, CancellationToken token) - { - var result = await repository.AddRange(idDiscriminator, sets, token); - return Ok(result); - } - - /// - /// Получение данных с фильтрацией. Значение фильтра null - отключен - /// - /// Дискриминатор (идентификатор) набора - /// Фильтр позднее даты - /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора - /// - /// - /// - /// Фильтрованный набор данных с сортировкой по отметке времени - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - public async Task Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, [FromQuery] IEnumerable? columnNames, int skip, int take, CancellationToken token) - { - var result = await repository.Get(idDiscriminator, geTimestamp, columnNames, skip, take, token); - return Ok(result); - } - - /// - /// Получить последние данные - /// - /// Дискриминатор (идентификатор) набора - /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора - /// - /// - /// Фильтрованный набор данных с сортировкой по отметке времени - [HttpGet("last")] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - public async Task GetLast(Guid idDiscriminator, [FromQuery] IEnumerable? columnNames, int take, CancellationToken token) - { - var result = await repository.GetLast(idDiscriminator, columnNames, take, token); - return Ok(result); - } - - /// - /// Диапазон дат за которые есть данные - /// - /// - /// - /// Дата первой и последней записи - [HttpGet("datesRange")] - [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) - { - var result = await repository.GetDatesRange(idDiscriminator, token); - return Ok(result); - } - - /// - /// Количество записей по указанному набору в БД. Для пагинации. - /// - /// Дискриминатор (идентификатор) набора - /// - /// - [HttpGet("count")] - [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] - [ProducesResponseType((int)HttpStatusCode.NoContent)] - public async Task Count(Guid idDiscriminator, CancellationToken token) - { - var result = await repository.Count(idDiscriminator, token); - return Ok(result); - } -} diff --git a/DD.Persistence.API/Controllers/TimestampedValuesController.cs b/DD.Persistence.API/Controllers/TimestampedValuesController.cs new file mode 100644 index 0000000..abc1114 --- /dev/null +++ b/DD.Persistence.API/Controllers/TimestampedValuesController.cs @@ -0,0 +1,151 @@ +using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using DD.Persistence.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace DD.Persistence.API.Controllers; + +/// +/// Хранение наборов данных с отметкой времени. +/// +[ApiController] +[Authorize] +[Route("api/[controller]/{discriminatorId}")] +public class TimestampedValuesController : ControllerBase +{ + private readonly ITimestampedValuesService timestampedValuesRepository; + + public TimestampedValuesController(ITimestampedValuesService repository) + { + this.timestampedValuesRepository = repository; + } + + /// + /// Записать новые данные. + /// Предполагается что данные с одним дискриминатором имеют одинаковую структуру + /// + /// Дискриминатор (идентификатор) набора + /// + /// + [HttpPost] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] + public async Task AddRange([FromRoute] Guid discriminatorId, [FromBody] IEnumerable dtos, CancellationToken token) + { + var result = await timestampedValuesRepository.AddRange(discriminatorId, dtos, token); + + return CreatedAtAction(nameof(AddRange), result); + } + + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора + /// + /// + /// + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task>> Get([FromRoute] Guid discriminatorId, DateTimeOffset? timestampBegin, [FromQuery] string[]? columnNames, int skip, int take, CancellationToken token) + { + var result = await timestampedValuesRepository.Get(discriminatorId, timestampBegin, columnNames, skip, take, token); + + return result.Any() ? Ok(result) : NoContent(); + } + + /// + /// Получить данные, начиная с заданной отметки времени + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// + [HttpGet("gtdate")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task>> GetGtDate([FromRoute] Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token) + { + var result = await timestampedValuesRepository.GetGtDate(discriminatorId, timestampBegin, token); + + return result.Any() ? Ok(result) : NoContent(); + } + + /// + /// Получить данные c начала + /// + /// Дискриминатор (идентификатор) набора + /// + /// + [HttpGet("first")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task>> GetFirst([FromRoute] Guid discriminatorId, int take, CancellationToken token) + { + var result = await timestampedValuesRepository.GetFirst(discriminatorId, take, token); + + return result.Any() ? Ok(result) : NoContent(); + } + + /// + /// Получить данные c конца + /// + /// Дискриминатор (идентификатор) набора + /// + /// + [HttpGet("last")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task>> GetLast([FromRoute] Guid discriminatorId, int take, CancellationToken token) + { + var result = await timestampedValuesRepository.GetLast(discriminatorId, take, token); + + return result.Any() ? Ok(result) : NoContent(); + } + + /// + /// Получить список объектов с прореживанием, удовлетворяющий диапазону дат + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// + /// + /// + [HttpGet("resampled")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task>> GetResampledData([FromRoute] Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default) + { + var result = await timestampedValuesRepository.GetResampledData(discriminatorId, timestampBegin, intervalSec, approxPointsCount, token); + + return result.Any() ? Ok(result) : NoContent(); + } + + /// + /// Получить количество записей по указанному набору в БД. Для пагинации + /// + /// Дискриминатор (идентификатор) набора + /// + [HttpGet("count")] + public async Task> Count([FromRoute] Guid discriminatorId, CancellationToken token) + { + var result = await timestampedValuesRepository.Count(discriminatorId, token); + + return Ok(result); + } + + /// + /// Получить диапазон дат, в пределах которых хранятся даные + /// + /// + /// + [HttpGet("datesRange")] + public async Task> GetDatesRange([FromRoute] Guid discriminatorId, CancellationToken token) + { + var result = await timestampedValuesRepository.GetDatesRange(discriminatorId, token); + + return Ok(result); + } +} diff --git a/DD.Persistence.API/Controllers/WitsDataController.cs b/DD.Persistence.API/Controllers/WitsDataController.cs index 5df6c3f..c87345e 100644 --- a/DD.Persistence.API/Controllers/WitsDataController.cs +++ b/DD.Persistence.API/Controllers/WitsDataController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using DD.Persistence.Models; using DD.Persistence.Services.Interfaces; using System.Net; +using DD.Persistence.Models.Common; namespace DD.Persistence.API.Controllers; diff --git a/DD.Persistence.API/DependencyInjection.cs b/DD.Persistence.API/DependencyInjection.cs index b0a71d5..b543841 100644 --- a/DD.Persistence.API/DependencyInjection.cs +++ b/DD.Persistence.API/DependencyInjection.cs @@ -16,12 +16,6 @@ namespace DD.Persistence.API; public static class DependencyInjection { - //public static void MapsterSetup() - //{ - // TypeAdapterConfig.GlobalSettings.Default.Config - // .ForType() - // .Ignore(dest => dest.System, dest => dest.SystemId); - //} public static void AddSwagger(this IServiceCollection services, IConfiguration configuration) { services.AddSwaggerGen(c => @@ -59,6 +53,7 @@ public static class DependencyInjection public static void AddServices(this IServiceCollection services) { services.AddTransient(); + services.AddTransient(); } #region Authentication diff --git a/DD.Persistence.API/Readme.md b/DD.Persistence.API/Readme.md new file mode 100644 index 0000000..43bbbf3 --- /dev/null +++ b/DD.Persistence.API/Readme.md @@ -0,0 +1,51 @@ +# Persistence Service Readme + +## Краткое описание +Persistence сервис отвечает за работу с хранимыми данными +в рамках совокупности различных систем. + +## Локальное развертывание +1. Скачать репозиторий по SSH +``` +ssh://git@git.ddrilling.ru:2221/on.nemtina/persistence.git +``` + +Для доступа к репозиториям редварительно необходимо сгенерировать SSH ключ и добавить его в Gitea + +2. Выбрать ветку dev + +## Использование Swagger-а +1. Сконфигурировать appsettings.Development.json +(при отсутствии) занести флаг: +```json +"NeedUseKeyCloak": true +``` +2. Запустить решение в режиме Debug +3. Выполнить авторизацию через KeyCloak - качестве client_id указать: +``` +webapi +``` +После этого должен произойти редирект на страницу авторизации в KeyCloak + +4. Заполнить поля и авторизоваться +``` +Username or email: myuser +``` +``` +Password: 12345 +``` + +## Тестирование +Запуск тестов рекомендуется осуществлять без использования KeyCloak
Для этого +настройка appsettings.Tests.json должна содержать: +``` +"NeedUseKeyCloak": false, +"AuthUser": { + "username": "myuser", + "password": 12345, + "clientId": "webapi", + "grantType": "password" +} +``` + + diff --git a/DD.Persistence.API/Startup.cs b/DD.Persistence.API/Startup.cs index c6c44a4..c5e8c5a 100644 --- a/DD.Persistence.API/Startup.cs +++ b/DD.Persistence.API/Startup.cs @@ -26,8 +26,6 @@ public class Startup services.AddJWTAuthentication(Configuration); services.AddMemoryCache(); services.AddServices(); - - //DependencyInjection.MapsterSetup(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/DD.Persistence.App/appsettings.json b/DD.Persistence.App/appsettings.json index 8002fc7..7ad8c67 100644 --- a/DD.Persistence.App/appsettings.json +++ b/DD.Persistence.App/appsettings.json @@ -19,6 +19,7 @@ "password": 12345, "clientId": "webapi", "grantType": "password", - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6" - } + "http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6" + }, + "ClientUrl": "http://localhost:5000/" } diff --git a/DD.Persistence.Client/Clients/ChangeLogClient.cs b/DD.Persistence.Client/Clients/ChangeLogClient.cs index e10008e..e4f5904 100644 --- a/DD.Persistence.Client/Clients/ChangeLogClient.cs +++ b/DD.Persistence.Client/Clients/ChangeLogClient.cs @@ -3,16 +3,18 @@ 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; namespace DD.Persistence.Client.Clients; public class ChangeLogClient : BaseClient, IChangeLogClient { - private readonly Interfaces.Refit.IRefitChangeLogClient refitChangeLogClient; + private readonly IRefitChangeLogClient refitChangeLogClient; - public ChangeLogClient(Interfaces.Refit.IRefitChangeLogClient refitChangeLogClient, ILogger logger) : base(logger) + public ChangeLogClient(IRefitClientFactory refitClientFactory, ILogger logger) : base(logger) { - this.refitChangeLogClient = refitChangeLogClient; - } + this.refitChangeLogClient = refitClientFactory.Create(); + } public async Task ClearAndAddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token) { diff --git a/DD.Persistence.Client/Clients/DataSourceSystemClient.cs b/DD.Persistence.Client/Clients/DataSourceSystemClient.cs index 45e2d29..7e70255 100644 --- a/DD.Persistence.Client/Clients/DataSourceSystemClient.cs +++ b/DD.Persistence.Client/Clients/DataSourceSystemClient.cs @@ -9,9 +9,9 @@ public class DataSourceSystemClient : BaseClient, IDataSourceSystemClient { private readonly IRefitDataSourceSystemClient dataSourceSystemClient; - public DataSourceSystemClient(IRefitDataSourceSystemClient dataSourceSystemClient, ILogger logger) : base(logger) + public DataSourceSystemClient(IRefitClientFactory dataSourceSystemClientFactory, ILogger logger) : base(logger) { - this.dataSourceSystemClient = dataSourceSystemClient; + this.dataSourceSystemClient = dataSourceSystemClientFactory.Create(); } public async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token) diff --git a/DD.Persistence.Client/Clients/Interfaces/IChangeLogClient.cs b/DD.Persistence.Client/Clients/Interfaces/IChangeLogClient.cs index 19edeab..f81ac8d 100644 --- a/DD.Persistence.Client/Clients/Interfaces/IChangeLogClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/IChangeLogClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; namespace DD.Persistence.Client.Clients.Interfaces; diff --git a/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs b/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs index 86462ea..407eb1c 100644 --- a/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients.Interfaces; @@ -24,12 +25,21 @@ public interface ISetpointClient : IDisposable /// Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRangeAsync(CancellationToken token); + /// + /// Получить актуальные значения уставок + /// + /// + /// + /// s + Task> GetCurrentDictionary(IEnumerable setpointConfigs, CancellationToken token); + + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + Task GetDatesRangeAsync(CancellationToken token); /// /// Получить значения уставок за определенный момент времени diff --git a/DD.Persistence.Client/Clients/Interfaces/ITechMessagesClient.cs b/DD.Persistence.Client/Clients/Interfaces/ITechMessagesClient.cs index a27e553..2af1af6 100644 --- a/DD.Persistence.Client/Clients/Interfaces/ITechMessagesClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/ITechMessagesClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; namespace DD.Persistence.Client.Clients.Interfaces; diff --git a/DD.Persistence.Client/Clients/Interfaces/ITimeSeriesClient.cs b/DD.Persistence.Client/Clients/Interfaces/ITimeSeriesClient.cs deleted file mode 100644 index 26bbfa6..0000000 --- a/DD.Persistence.Client/Clients/Interfaces/ITimeSeriesClient.cs +++ /dev/null @@ -1,44 +0,0 @@ -using DD.Persistence.Models; - -namespace DD.Persistence.Client.Clients.Interfaces; - -/// -/// Клиент для работы с временными данными -/// -/// -public interface ITimeSeriesClient : IDisposable where TDto : class, new() -{ - /// - /// Добавление записей - /// - /// - /// - /// - Task AddRange(IEnumerable dtos, CancellationToken token); - - /// - /// Получить список объектов, удовлетворяющий диапазону дат - /// - /// - /// - /// - /// - Task> Get(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); - - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRange(CancellationToken token); - - /// - /// Получить список объектов с прореживанием, удовлетворяющий диапазону дат - /// - /// - /// - /// - /// - /// - Task> GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default); -} \ No newline at end of file diff --git a/DD.Persistence.Client/Clients/Interfaces/ITimestampedSetClient.cs b/DD.Persistence.Client/Clients/Interfaces/ITimestampedSetClient.cs deleted file mode 100644 index be98a77..0000000 --- a/DD.Persistence.Client/Clients/Interfaces/ITimestampedSetClient.cs +++ /dev/null @@ -1,59 +0,0 @@ -using DD.Persistence.Models; - -namespace DD.Persistence.Client.Clients.Interfaces; - -/// -/// Клиент для работы с репозиторием для хранения разных наборов данных рядов. -/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. -/// idDiscriminator формируют клиенты и только им известно что они обозначают. -/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. -/// -public interface ITimestampedSetClient : IDisposable -{ - /// - /// Записать новые данные - /// - /// - /// - /// - /// - Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); - - /// - /// Количество записей по указанному набору в БД. Для пагинации - /// - /// - /// - /// - Task Count(Guid idDiscriminator, CancellationToken token); - - /// - /// Получение данных с фильтрацией. Значение фильтра null - отключен - /// - /// - /// - /// - /// - /// - /// - /// - Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); - - /// - /// Диапазон дат за которые есть данные - /// - /// - /// - /// - Task GetDatesRange(Guid idDiscriminator, CancellationToken token); - - /// - /// - /// - /// - /// - /// - /// - /// - Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token); -} \ No newline at end of file diff --git a/DD.Persistence.Client/Clients/Interfaces/ITimestampedValuesClient.cs b/DD.Persistence.Client/Clients/Interfaces/ITimestampedValuesClient.cs new file mode 100644 index 0000000..b0ea180 --- /dev/null +++ b/DD.Persistence.Client/Clients/Interfaces/ITimestampedValuesClient.cs @@ -0,0 +1,103 @@ +using DD.Persistence.Models; +using DD.Persistence.Models.Common; + +namespace DD.Persistence.Client.Clients.Interfaces; + +/// +/// Клиент для работы с наборами данных, имеющими отметку времени. +/// discriminatorId - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// discriminatorId формируют клиенты и только им известно что они обозначают. +/// +public interface ITimestampedValuesClient : IDisposable +{ + /// + /// Записать новые данные + /// Предполагается что данные с одним дискриминатором имеют одинаковую структуру + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token); + + /// + /// Получить данные с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора + /// + /// + /// + Task> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable? columnNames, int skip, int take, CancellationToken token); + + /// + /// Получить данные, начиная с заданной отметки времени + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// + Task> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token); + + /// + /// Получить данные с начала + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task> GetFirst(Guid discriminatorId, int take, CancellationToken token); + + /// + /// Получить данные с конца + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task> GetLast(Guid discriminatorId, int take, CancellationToken token); + + /// + /// Получить данные с прореживанием, удовлетворяющем диапазону дат + /// + /// Дискриминатор (идентификатор) набора + /// + /// + /// + /// + Task> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default); + + /// + /// Количество записей по указанному набору в БД. Для пагинации + /// + /// Дискриминатор (идентификатор) набора + /// + Task Count(Guid discriminatorId, CancellationToken token); + + /// + /// Диапазон дат, в пределах которых осуществляется хранение данных + /// + /// Дискриминатор (идентификатор) набора + /// + Task GetDatesRange(Guid discriminatorId, CancellationToken token); + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); + + /// + /// + /// + /// + /// + /// + /// + /// + Task> GetLast(Guid idDiscriminator, int take, CancellationToken token); +} \ No newline at end of file diff --git a/DD.Persistence.Client/Clients/Interfaces/IWitsDataClient.cs b/DD.Persistence.Client/Clients/Interfaces/IWitsDataClient.cs index e954484..0ac015d 100644 --- a/DD.Persistence.Client/Clients/Interfaces/IWitsDataClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/IWitsDataClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using Refit; namespace DD.Persistence.Client.Clients.Interfaces; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs index 958e24e..8e35b4e 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs @@ -1,10 +1,11 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; using Refit; namespace DD.Persistence.Client.Clients.Interfaces.Refit; -public interface IRefitChangeLogClient : IDisposable +public interface IRefitChangeLogClient : IRefitClient, IDisposable { private const string BaseRoute = "/api/ChangeLog"; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs new file mode 100644 index 0000000..4a140e0 --- /dev/null +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DD.Persistence.Client.Clients.Interfaces.Refit; + +/// +/// Базовый Refit интерфейс +/// +public interface IRefitClient +{ +} diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitDataSourceSystemClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitDataSourceSystemClient.cs index a4b47eb..313d8cc 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitDataSourceSystemClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitDataSourceSystemClient.cs @@ -2,7 +2,7 @@ using Refit; namespace DD.Persistence.Client.Clients.Interfaces.Refit; -public interface IRefitDataSourceSystemClient : IDisposable +public interface IRefitDataSourceSystemClient : IRefitClient, IDisposable { private const string BaseRoute = "/api/dataSourceSystem"; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs index b6e021a..7931e6d 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs @@ -1,14 +1,19 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using Refit; +using System.Text.Json; namespace DD.Persistence.Client.Clients.Interfaces.Refit; -public interface IRefitSetpointClient : IDisposable +public interface IRefitSetpointClient : IRefitClient, IDisposable { private const string BaseRoute = "/api/setpoint"; + //[Get($"{BaseRoute}/current")] + //Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); + [Get($"{BaseRoute}/current")] - Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); + Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); [Get($"{BaseRoute}/history")] Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment, CancellationToken token); diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs index 2638600..ef9496e 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs @@ -1,11 +1,11 @@ -using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; +using DD.Persistence.Models; using DD.Persistence.Models.Requests; using Refit; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients.Interfaces.Refit { - public interface IRefitTechMessagesClient : IDisposable + public interface IRefitTechMessagesClient : IRefitClient, IDisposable { private const string BaseRoute = "/api/techMessages"; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimeSeriesClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimeSeriesClient.cs deleted file mode 100644 index 2edc8fe..0000000 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimeSeriesClient.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DD.Persistence.Models; -using Refit; - -namespace DD.Persistence.Client.Clients.Interfaces.Refit; -public interface IRefitTimeSeriesClient : IDisposable - where TDto : class, new() -{ - private const string BaseRoute = "/api/dataSaub"; - - [Post($"{BaseRoute}")] - Task> AddRange(IEnumerable dtos, CancellationToken token); - - [Get($"{BaseRoute}")] - Task>> Get(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); - - [Get($"{BaseRoute}/resampled")] - Task>> GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default); - - [Get($"{BaseRoute}/datesRange")] - Task> GetDatesRange(CancellationToken token); -} diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedSetClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedSetClient.cs deleted file mode 100644 index 14db284..0000000 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedSetClient.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DD.Persistence.Models; -using Refit; - -namespace DD.Persistence.Client.Clients.Interfaces.Refit; - -public interface IRefitTimestampedSetClient : IDisposable -{ - private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}"; - - [Post(baseUrl)] - Task> AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); - - [Get(baseUrl)] - Task>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable? columnNames, int skip, int take, CancellationToken token); - - [Get($"{baseUrl}/last")] - Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? columnNames, int take, CancellationToken token); - - [Get($"{baseUrl}/count")] - Task> Count(Guid idDiscriminator, CancellationToken token); - - [Get($"{baseUrl}/datesRange")] - Task> GetDatesRange(Guid idDiscriminator, CancellationToken token); -} diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedValuesClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedValuesClient.cs new file mode 100644 index 0000000..bd94136 --- /dev/null +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTimestampedValuesClient.cs @@ -0,0 +1,61 @@ +using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using Refit; + +namespace DD.Persistence.Client.Clients.Interfaces.Refit; + +/// +/// Refit интерфейс для TimestampedValuesController +/// +public interface IRefitTimestampedValuesClient : IRefitClient, IDisposable +{ + private const string baseUrl = "/api/TimestampedValues/{discriminatorId}"; + + /// + /// Записать новые данные + /// + [Post(baseUrl)] + Task> AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token); + + /// + /// Получение данных с фильтрацией + /// + [Get(baseUrl)] + Task>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, [Query(CollectionFormat.Multi)] IEnumerable? columnNames, int skip, int take, CancellationToken token); + + /// + /// Получить данные, начиная с заданной отметки времени + /// + [Get($"{baseUrl}/gtdate")] + Task>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token); + + /// + /// Получить данные c начала + /// + [Get($"{baseUrl}/first")] + Task>> GetFirst(Guid discriminatorId, int take, CancellationToken token); + + /// + /// Получить данные c конца + /// + [Get($"{baseUrl}/last")] + Task>> GetLast(Guid discriminatorId, int take, CancellationToken token); + + /// + /// Получить список объектов с прореживанием, удовлетворяющий диапазону временных отметок + /// + [Get($"{baseUrl}/resampled")] + Task>> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default); + + /// + /// Получить количество записей по указанному набору в БД. Для пагинации + /// + [Get($"{baseUrl}/count")] + Task> Count(Guid discriminatorId, CancellationToken token); + + /// + /// Получить диапазон дат, в пределах которых хранятся даные + /// + [Get($"{baseUrl}/datesRange")] + Task> GetDatesRange(Guid discriminatorId, CancellationToken token); +} diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs index 57bff6f..233083a 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs @@ -1,9 +1,9 @@ -using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; +using DD.Persistence.Models; using Refit; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients.Interfaces.Refit; -public interface IRefitWitsDataClient : IDisposable +public interface IRefitWitsDataClient : IRefitClient, IDisposable { private const string BaseRoute = "/api/witsData"; diff --git a/DD.Persistence.Client/Clients/SetpointClient.cs b/DD.Persistence.Client/Clients/SetpointClient.cs index 4a990c5..dfb5026 100644 --- a/DD.Persistence.Client/Clients/SetpointClient.cs +++ b/DD.Persistence.Client/Clients/SetpointClient.cs @@ -1,33 +1,60 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; 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; public class SetpointClient : BaseClient, ISetpointClient { private readonly IRefitSetpointClient refitSetpointClient; + private readonly ISetpointConfigStorage setpointConfigStorage; - public SetpointClient(IRefitSetpointClient refitSetpointClient, ILogger logger) : base(logger) + public SetpointClient( + IRefitClientFactory refitSetpointClientFactory, + ISetpointConfigStorage setpointConfigStorage, + ILogger logger) : base(logger) { - this.refitSetpointClient = refitSetpointClient; - } + this.refitSetpointClient = refitSetpointClientFactory.Create(); + this.setpointConfigStorage = setpointConfigStorage; + } public async Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token) { var result = await ExecuteGetResponse( async () => await refitSetpointClient.GetCurrent(setpointKeys, token), token); - return result!; + return result!.Select(x => new SetpointValueDto { + Key = x.Key, + Value = DeserializeValue(x.Key, x.Value) + }); } - public async Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + + + public async Task> GetCurrentDictionary(IEnumerable setpointConfigs, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitSetpointClient.GetCurrent(setpointConfigs, token), token); + + + return result!.ToDictionary(x => x.Key,x => DeserializeValue(x.Key,x.Value)); + } + + public async Task> GetHistory(IEnumerable 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!; } @@ -36,6 +63,9 @@ 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!; } @@ -48,14 +78,18 @@ public class SetpointClient : BaseClient, ISetpointClient } public async Task> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await refitSetpointClient.GetPart(dateBegin, take, token), token); + { + var result = await ExecuteGetResponse( + async () => await refitSetpointClient.GetPart(dateBegin, take, token), token); - return result!; - } + DeserializeList(result); - public async Task Add(Guid setpointKey, object newValue, CancellationToken token) + return result!; + } + + + + public async Task Add(Guid setpointKey, object newValue, CancellationToken token) { await ExecutePostResponse( async () => await refitSetpointClient.Add(setpointKey, newValue, token), token); @@ -67,4 +101,21 @@ 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? result) + { + foreach (var log in result) + log.Value = DeserializeValue(log.Key, (JsonElement)log.Value); + + } + + } diff --git a/DD.Persistence.Client/Clients/TechMessagesClient.cs b/DD.Persistence.Client/Clients/TechMessagesClient.cs index c981569..681e275 100644 --- a/DD.Persistence.Client/Clients/TechMessagesClient.cs +++ b/DD.Persistence.Client/Clients/TechMessagesClient.cs @@ -4,6 +4,7 @@ using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Models; using DD.Persistence.Models.Requests; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients; @@ -11,9 +12,9 @@ public class TechMessagesClient : BaseClient, ITechMessagesClient { private readonly IRefitTechMessagesClient refitTechMessagesClient; - public TechMessagesClient(IRefitTechMessagesClient refitTechMessagesClient, ILogger logger) : base(logger) + public TechMessagesClient(IRefitClientFactory refitTechMessagesClientFactory, ILogger logger) : base(logger) { - this.refitTechMessagesClient = refitTechMessagesClient; + this.refitTechMessagesClient = refitTechMessagesClientFactory.Create(); } public async Task> GetPage(PaginationRequest request, CancellationToken token) diff --git a/DD.Persistence.Client/Clients/TimeSeriesClient.cs b/DD.Persistence.Client/Clients/TimeSeriesClient.cs deleted file mode 100644 index 2c15938..0000000 --- a/DD.Persistence.Client/Clients/TimeSeriesClient.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.Extensions.Logging; -using DD.Persistence.Client.Clients.Base; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Client.Clients.Interfaces.Refit; -using DD.Persistence.Models; - -namespace DD.Persistence.Client.Clients; -public class TimeSeriesClient : BaseClient, ITimeSeriesClient where TDto : class, new() -{ - private readonly IRefitTimeSeriesClient timeSeriesClient; - - public TimeSeriesClient(IRefitTimeSeriesClient refitTechMessagesClient, ILogger> logger) : base(logger) - { - this.timeSeriesClient = refitTechMessagesClient; - } - - public async Task AddRange(IEnumerable dtos, CancellationToken token) - { - var result = await ExecutePostResponse( - async () => await timeSeriesClient.AddRange(dtos, token), token); - - return result; - } - - public async Task> Get(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await timeSeriesClient.Get(dateBegin, dateEnd, token), token); - - return result!; - } - - public async Task> GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default) - { - var result = await ExecuteGetResponse( - async () => await timeSeriesClient.GetResampledData(dateBegin, intervalSec, approxPointsCount, token), token); - - return result!; - } - - public async Task GetDatesRange(CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await timeSeriesClient.GetDatesRange(token), token); - - return result; - } - - public void Dispose() - { - timeSeriesClient.Dispose(); - - GC.SuppressFinalize(this); - } -} diff --git a/DD.Persistence.Client/Clients/TimestampedSetClient.cs b/DD.Persistence.Client/Clients/TimestampedSetClient.cs deleted file mode 100644 index 38828b6..0000000 --- a/DD.Persistence.Client/Clients/TimestampedSetClient.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.Extensions.Logging; -using DD.Persistence.Client.Clients.Base; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Client.Clients.Interfaces.Refit; -using DD.Persistence.Models; - -namespace DD.Persistence.Client.Clients; -public class TimestampedSetClient : BaseClient, ITimestampedSetClient -{ - private readonly IRefitTimestampedSetClient refitTimestampedSetClient; - - public TimestampedSetClient(IRefitTimestampedSetClient refitTimestampedSetClient, ILogger logger) : base(logger) - { - this.refitTimestampedSetClient = refitTimestampedSetClient; - } - - public async Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) - { - var result = await ExecutePostResponse( - async () => await refitTimestampedSetClient.AddRange(idDiscriminator, sets, token), token); - - return result; - } - - public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await refitTimestampedSetClient.Get(idDiscriminator, geTimestamp, columnNames, skip, take, token), token); - - return result!; - } - - public async Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await refitTimestampedSetClient.GetLast(idDiscriminator, columnNames, take, token), token); - - return result!; - } - - public async Task Count(Guid idDiscriminator, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await refitTimestampedSetClient.Count(idDiscriminator, token), token); - - return result; - } - - public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) - { - var result = await ExecuteGetResponse( - async () => await refitTimestampedSetClient.GetDatesRange(idDiscriminator, token), token); - - return result; - } - - public void Dispose() - { - refitTimestampedSetClient.Dispose(); - - GC.SuppressFinalize(this); - } -} diff --git a/DD.Persistence.Client/Clients/TimestampedValuesClient.cs b/DD.Persistence.Client/Clients/TimestampedValuesClient.cs new file mode 100644 index 0000000..bafead6 --- /dev/null +++ b/DD.Persistence.Client/Clients/TimestampedValuesClient.cs @@ -0,0 +1,127 @@ +using DD.Persistence.Client.Clients.Base; +using DD.Persistence.Client.Clients.Interfaces; +using DD.Persistence.Client.Clients.Interfaces.Refit; +using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace DD.Persistence.Client.Clients; + +/// +public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient +{ + private readonly IRefitTimestampedValuesClient refitTimestampedSetClient; + + /// + public TimestampedValuesClient(IRefitClientFactory refitTimestampedSetClientFactory, ILogger logger) : base(logger) + { + this.refitTimestampedSetClient = refitTimestampedSetClientFactory.Create(); + } + + /// + private readonly ConcurrentDictionary mapperCache = new(); + + /// + public async Task AddRange(Guid discriminatorId, IEnumerable sets, CancellationToken token) + { + var result = await ExecutePostResponse( + async () => await refitTimestampedSetClient.AddRange(discriminatorId, sets, token), token); + + return result; + } + + /// + public async Task> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.Get(discriminatorId, geTimestamp, columnNames, skip, take, token), token); + return result; + } + + /// + public async Task> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.GetGtDate(discriminatorId, timestampBegin, token), token); + + return result; + } + + /// + public async Task> GetFirst(Guid discriminatorId, int take, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.GetFirst(discriminatorId, take, token), token); + + return result; + } + + /// + public async Task> GetLast(Guid discriminatorId, int take, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.GetLast(discriminatorId, take, token), token); + + return result; + } + + /// + public async Task> GetResampledData(Guid discriminatorId, DateTimeOffset dateBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.GetResampledData(discriminatorId, dateBegin, intervalSec, approxPointsCount, token), token); + + return result; + } + + /// + public async Task Count(Guid discriminatorId, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.Count(discriminatorId, token), token); + + return result; + } + + /// + public async Task GetDatesRange(Guid discriminatorId, CancellationToken token) + { + var result = await ExecuteGetResponse( + async () => await refitTimestampedSetClient.GetDatesRange(discriminatorId, token), token); + + return result; + } + + /// + public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) + { + var data = await Get(idDiscriminator, geTimestamp, columnNames, skip, take, token); + var mapper = GetMapper(idDiscriminator); + + return data.Select(mapper.DeserializeTimeStampedData); + } + + /// + public async Task> GetLast(Guid idDiscriminator, int take, CancellationToken token) + { + var data = await GetLast(idDiscriminator, take, token); + var mapper = GetMapper(idDiscriminator); + + return data.Select(mapper.DeserializeTimeStampedData); + } + + /// + private TimestampedSetMapper GetMapper(Guid idDiscriminator) + { + return (TimestampedSetMapper)mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper(idDiscriminator)); + } + + /// + public void Dispose() + { + refitTimestampedSetClient.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/DD.Persistence.Client/Clients/WitsDataClient.cs b/DD.Persistence.Client/Clients/WitsDataClient.cs index 3251194..d47f771 100644 --- a/DD.Persistence.Client/Clients/WitsDataClient.cs +++ b/DD.Persistence.Client/Clients/WitsDataClient.cs @@ -3,15 +3,16 @@ using DD.Persistence.Client.Clients.Base; using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients; public class WitsDataClient : BaseClient, IWitsDataClient { private readonly IRefitWitsDataClient refitWitsDataClient; - public WitsDataClient(IRefitWitsDataClient refitWitsDataClient, ILogger logger) : base(logger) + public WitsDataClient(IRefitClientFactory refitWitsDataClientFactory, ILogger logger) : base(logger) { - this.refitWitsDataClient = refitWitsDataClient; + this.refitWitsDataClient = refitWitsDataClientFactory.Create(); } public async Task AddRange(IEnumerable dtos, CancellationToken token) diff --git a/DD.Persistence.Client/DD.Persistence.Client.csproj b/DD.Persistence.Client/DD.Persistence.Client.csproj index d1ae186..1e0bc4a 100644 --- a/DD.Persistence.Client/DD.Persistence.Client.csproj +++ b/DD.Persistence.Client/DD.Persistence.Client.csproj @@ -9,13 +9,13 @@ True - Persistence.Client + DD.Persistence.Client - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1 - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1 - Persistence.Client + DD.Persistence.Client Digital Drilling @@ -33,15 +33,15 @@ snupkg - C:\Projects\Nuget + C:\Projects\Nuget\Persistence Readme.md - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.5.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) @@ -53,11 +53,12 @@ + - + diff --git a/DD.Persistence.Client/DependencyInjection.cs b/DD.Persistence.Client/DependencyInjection.cs new file mode 100644 index 0000000..89f65cf --- /dev/null +++ b/DD.Persistence.Client/DependencyInjection.cs @@ -0,0 +1,34 @@ +using DD.Persistence.Client.Clients; +using DD.Persistence.Client.Clients.Interfaces; +using DD.Persistence.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace DD.Persistence.Client; + +/// +/// +/// +public static class DependencyInjection +{ + /// + /// + /// + /// + /// + public static IServiceCollection AddPersistenceClients(this IServiceCollection services, Dictionary? setpointTypeConfigs = null) + { + services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>)); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddSingleton(provider => + { + return new SetpointConfigStorage(setpointTypeConfigs); + }); + return services; + } +} diff --git a/DD.Persistence.Client/IRefitClientFactory.cs b/DD.Persistence.Client/IRefitClientFactory.cs new file mode 100644 index 0000000..0015a95 --- /dev/null +++ b/DD.Persistence.Client/IRefitClientFactory.cs @@ -0,0 +1,16 @@ +using DD.Persistence.Client.Clients.Interfaces.Refit; + +namespace DD.Persistence.Client; + +/// +/// Интерфейс для фабрики, которая создает refit-клиентов +/// +/// +public interface IRefitClientFactory where T : IRefitClient +{ + /// + /// Создание refit-клиента + /// + /// + public T Create(); +} diff --git a/DD.Persistence.Client/ISetpointConfigStorage.cs b/DD.Persistence.Client/ISetpointConfigStorage.cs new file mode 100644 index 0000000..2c73783 --- /dev/null +++ b/DD.Persistence.Client/ISetpointConfigStorage.cs @@ -0,0 +1,11 @@ +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); +} diff --git a/DD.Persistence.Client/PersistenceClientFactory.cs b/DD.Persistence.Client/PersistenceClientFactory.cs deleted file mode 100644 index ad7c817..0000000 --- a/DD.Persistence.Client/PersistenceClientFactory.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Microsoft.Extensions.Configuration; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Client.Clients; -using DD.Persistence.Client.Helpers; -using Refit; -using DD.Persistence.Factories; -using DD.Persistence.Client.Clients.Interfaces.Refit; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using System.Text.Json; - -namespace DD.Persistence.Client -{ - /// - /// Фабрика клиентов для доступа к Persistence - сервису - /// - public class PersistenceClientFactory - { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true - }; - private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions)); - private readonly IServiceProvider provider; - private HttpClient httpClient; - public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IServiceProvider provider, IConfiguration configuration) - { - this.provider = provider; - - httpClient = httpClientFactory.CreateClient(); - - httpClient.Authorize(configuration); - } - - public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IAuthTokenFactory authTokenFactory, IServiceProvider provider, IConfiguration configuration) - { - this.provider = provider; - - httpClient = httpClientFactory.CreateClient(); - - var token = authTokenFactory.GetToken(); - httpClient.Authorize(token); - } - - /// - /// Получить клиент для работы с уставками - /// - /// - public ISetpointClient GetSetpointClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new SetpointClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы с технологическими сообщениями - /// - /// - public ITechMessagesClient GetTechMessagesClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new TechMessagesClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы с временными данными - /// - /// - /// - public ITimeSeriesClient GetTimeSeriesClient() - where TDto : class, new() - { - var logger = provider.GetRequiredService>>(); - - var restClient = RestService.For>(httpClient, RefitSettings); - var client = new TimeSeriesClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы с данными с отметкой времени - /// - /// - public ITimestampedSetClient GetTimestampedSetClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new TimestampedSetClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы с записями ChangeLog - /// - /// - public IChangeLogClient GetChangeLogClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new ChangeLogClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы c параметрами Wits - /// - /// - public IWitsDataClient GetWitsDataClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new WitsDataClient(restClient, logger); - - return client; - } - - /// - /// Получить клиент для работы c системами - /// - /// - public IDataSourceSystemClient GetDataSourceSystemClient() - { - var logger = provider.GetRequiredService>(); - - var restClient = RestService.For(httpClient, RefitSettings); - var client = new DataSourceSystemClient(restClient, logger); - - return client; - } - } -} diff --git a/DD.Persistence.Client/Readme.md b/DD.Persistence.Client/Readme.md index 1e09e2f..6bdae87 100644 --- a/DD.Persistence.Client/Readme.md +++ b/DD.Persistence.Client/Readme.md @@ -11,8 +11,7 @@ Persistence сервисом посредством обращения к кон ## Список предоставляемых клиентов - `ISetpointClient` - Клиент для работы с уставками - `ITechMessagesClient` - Клиент для работы с технологическими сообщениями -- `ITimeSeriesClient` - Клиент для работы с временными данными -- `ITimestampedSetClient` - Клиент для работы с данными с отметкой времени +- `ITimestampedValuesClient` - Клиент для работы с наборами данных, имеющими отметку времени - `IChangeLogClient` - Клиент для работы с записями ChangeLog - `IWitsDataClient` - Клиент для работы с параметрами Wits - `IDataSourceSystemClient` - Клиент для работы с системами diff --git a/DD.Persistence.Client/RefitClientFactory.cs b/DD.Persistence.Client/RefitClientFactory.cs new file mode 100644 index 0000000..9cd0bef --- /dev/null +++ b/DD.Persistence.Client/RefitClientFactory.cs @@ -0,0 +1,52 @@ +using DD.Persistence.Client.Clients.Interfaces.Refit; +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; + +/// +/// Фабрика, которая создает refit-клиентов +/// +/// +public class RefitClientFactory : IRefitClientFactory where T : IRefitClient +{ + private HttpClient client; + private RefitSettings refitSettings; + + /// + public RefitClientFactory(IConfiguration configuration, ILogger> logger, HttpClient client) + { + //this.client = factory.CreateClient(); + this.client = client; + + var baseUrl = configuration.GetSection("ClientUrl").Get(); + if (String.IsNullOrEmpty(baseUrl)) + { + var exception = new SettingsPropertyNotFoundException("В настройках конфигурации не указан адрес Persistence сервиса."); + + logger.LogError(exception.Message); + + throw exception; + } + client.BaseAddress = new Uri(baseUrl); + + JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + refitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions)); + } + /// + /// создание клиента + /// + /// + public T Create() + { + return RestService.For(client, refitSettings); + } +} diff --git a/DD.Persistence.Client/SetpointConfigStorage.cs b/DD.Persistence.Client/SetpointConfigStorage.cs new file mode 100644 index 0000000..5cfbabf --- /dev/null +++ b/DD.Persistence.Client/SetpointConfigStorage.cs @@ -0,0 +1,20 @@ +namespace DD.Persistence.Client; +internal class SetpointConfigStorage : ISetpointConfigStorage +{ + private readonly Dictionary setpointTypeConfigs; + + public SetpointConfigStorage(Dictionary? setpointTypeConfigs) + { + this.setpointTypeConfigs = setpointTypeConfigs?? new Dictionary(); + } + + public bool TryGetType(Guid id, out Type type) + { + return setpointTypeConfigs.TryGetValue(id, out type); + } + + public void AddOrReplace(Guid id, Type type) + { + setpointTypeConfigs[id] = type; + } +} diff --git a/DD.Persistence.Client/TimestampedSetMapper.cs b/DD.Persistence.Client/TimestampedSetMapper.cs new file mode 100644 index 0000000..cf7463a --- /dev/null +++ b/DD.Persistence.Client/TimestampedSetMapper.cs @@ -0,0 +1,135 @@ +using DD.Persistence.Models; +using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace DD.Persistence.Client; + +internal abstract class TimestampedSetMapperBase +{ + public abstract object Map(TimestampedValuesDto data); + +} +internal class TimestampedSetMapper : TimestampedSetMapperBase +{ + private readonly Type entityType = typeof(T); + public Guid IdDiscriminator { get; } + private readonly ConcurrentDictionary PropertyCache = new(); + + public TimestampedSetMapper(Guid idDiscriminator) + { + IdDiscriminator = idDiscriminator; + } + + public override object Map(TimestampedValuesDto data) + { + return DeserializeTimeStampedData(data)!; + } + + public T DeserializeTimeStampedData(TimestampedValuesDto data) + { + + if (entityType.IsValueType) + return MapStruct(data); + else + return MapClass(data); + } + + private T MapClass(TimestampedValuesDto data) + { + var entity = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + foreach (var (propertyName, value) in data.Values) + { + if (value is JsonElement jsonElement) + SetPropertyValueFromJson(ref entity, propertyName, jsonElement); + } + SetPropertyValue(ref entity, "Timestamp", data.Timestamp); + return entity; + } + + private T MapStruct(TimestampedValuesDto data) + { + var entity = Activator.CreateInstance(); + 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); + + return (T)boxedEntity; + } + + private void SetPropertyValueForStructFromJson(ref object entity, string propertyName, JsonElement element) + { + var property = GetPropertyInfo(propertyName); + if (property is null) + return; + + try + { + var value = element.Deserialize(property.PropertyType); + property.SetValue(entity, value); + } + catch (Exception ex) + { + } + } + private void SetPropertyValueForStruct(ref object entity, string propertyName, object value) + { + var property = GetPropertyInfo(propertyName); + if (property is null) + return; + + try + { + var convertedValue = Convert.ChangeType(value, property.PropertyType); + property.SetValue(entity, convertedValue); + } + catch (Exception ex) + { + } + } + + + private void SetPropertyValueFromJson(ref T entity, string propertyName, JsonElement jsonElement) + { + var property = GetPropertyInfo(propertyName); + + if (property is null) + return; + + try + { + var value = jsonElement.Deserialize(property.PropertyType); + property.SetValue(entity, value); + } + catch (Exception ex) + { + + } + } + + private void SetPropertyValue(ref T entity, string propertyName, object value) + { + var property = GetPropertyInfo(propertyName); + if (property is null) + return; + + try + { + var convertedValue = Convert.ChangeType(value, property.PropertyType); + property.SetValue(entity, convertedValue); + } + catch (Exception ex) + { + } + } + + private PropertyInfo? GetPropertyInfo(string propertyName) + { + return PropertyCache.GetOrAdd(propertyName, name => entityType.GetProperty(name)); + } +} diff --git a/DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.Designer.cs b/DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.Designer.cs similarity index 67% rename from DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.Designer.cs rename to DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.Designer.cs index 2317c3b..81695fb 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.Designer.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.Designer.cs @@ -1,5 +1,6 @@ // using System; +using System.Text.Json; using DD.Persistence.Database.Model; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace DD.Persistence.Database.Postgres.Migrations { [DbContext(typeof(PersistencePostgresContext))] - [Migration("20241226122220_Init")] + [Migration("20250122120353_Init")] partial class Init { /// @@ -25,6 +26,23 @@ namespace DD.Persistence.Database.Postgres.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b => + { + b.Property("DiscriminatorId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Идентификатор схемы данных"); + + b.Property("PropNames") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Наименования полей в порядке индексации"); + + b.HasKey("DiscriminatorId"); + + b.ToTable("data_scheme"); + }); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => { b.Property("SystemId") @@ -105,27 +123,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("tech_message"); }); - modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedSet", b => + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => { - b.Property("IdDiscriminator") + b.Property("DiscriminatorId") .HasColumnType("uuid") - .HasComment("Дискриминатор ссылка на тип сохраняемых данных"); + .HasComment("Дискриминатор системы"); b.Property("Timestamp") .HasColumnType("timestamp with time zone") - .HasComment("Отметка времени, строго в UTC"); + .HasComment("Временная отметка"); - b.Property("Set") + b.Property("Values") .IsRequired() .HasColumnType("jsonb") - .HasComment("Набор сохраняемых данных"); + .HasComment("Данные"); - b.HasKey("IdDiscriminator", "Timestamp"); + b.HasKey("DiscriminatorId", "Timestamp"); - b.ToTable("timestamped_set", t => - { - t.HasComment("Общая таблица данных временных рядов"); - }); + b.ToTable("timestamped_values"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -181,96 +196,13 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("change_log"); }); - modelBuilder.Entity("DD.Persistence.Database.Model.DataSaub", b => - { - b.Property("Date") - .HasColumnType("timestamp with time zone") - .HasColumnName("date"); - - b.Property("AxialLoad") - .HasColumnType("double precision") - .HasColumnName("axialLoad"); - - b.Property("BitDepth") - .HasColumnType("double precision") - .HasColumnName("bitDepth"); - - b.Property("BlockPosition") - .HasColumnType("double precision") - .HasColumnName("blockPosition"); - - b.Property("BlockSpeed") - .HasColumnType("double precision") - .HasColumnName("blockSpeed"); - - b.Property("Flow") - .HasColumnType("double precision") - .HasColumnName("flow"); - - b.Property("HookWeight") - .HasColumnType("double precision") - .HasColumnName("hookWeight"); - - b.Property("IdFeedRegulator") - .HasColumnType("integer") - .HasColumnName("idFeedRegulator"); - - b.Property("Mode") - .HasColumnType("integer") - .HasColumnName("mode"); - - b.Property("Mse") - .HasColumnType("double precision") - .HasColumnName("mse"); - - b.Property("MseState") - .HasColumnType("smallint") - .HasColumnName("mseState"); - - b.Property("Pressure") - .HasColumnType("double precision") - .HasColumnName("pressure"); - - b.Property("Pump0Flow") - .HasColumnType("double precision") - .HasColumnName("pump0Flow"); - - b.Property("Pump1Flow") - .HasColumnType("double precision") - .HasColumnName("pump1Flow"); - - b.Property("Pump2Flow") - .HasColumnType("double precision") - .HasColumnName("pump2Flow"); - - b.Property("RotorSpeed") - .HasColumnType("double precision") - .HasColumnName("rotorSpeed"); - - b.Property("RotorTorque") - .HasColumnType("double precision") - .HasColumnName("rotorTorque"); - - b.Property("User") - .HasColumnType("text") - .HasColumnName("user"); - - b.Property("WellDepth") - .HasColumnType("double precision") - .HasColumnName("wellDepth"); - - b.HasKey("Date"); - - b.ToTable("data_saub"); - }); - modelBuilder.Entity("DD.Persistence.Database.Model.Setpoint", b => { b.Property("Key") .HasColumnType("uuid") .HasComment("Ключ"); - b.Property("Created") + b.Property("Timestamp") .HasColumnType("timestamp with time zone") .HasComment("Дата создания уставки"); @@ -278,12 +210,11 @@ namespace DD.Persistence.Database.Postgres.Migrations .HasColumnType("uuid") .HasComment("Id автора последнего изменения"); - b.Property("Value") - .IsRequired() + b.Property("Value") .HasColumnType("jsonb") .HasComment("Значение уставки"); - b.HasKey("Key", "Created"); + b.HasKey("Key", "Timestamp"); b.ToTable("setpoint"); }); @@ -298,6 +229,17 @@ namespace DD.Persistence.Database.Postgres.Migrations b.Navigation("System"); }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => + { + b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme") + .WithMany() + .HasForeignKey("DiscriminatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataScheme"); + }); #pragma warning restore 612, 618 } } diff --git a/DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.cs b/DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.cs similarity index 71% rename from DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.cs rename to DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.cs index d87210e..bc48d98 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20241226122220_Init.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250122120353_Init.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -33,32 +34,15 @@ namespace DD.Persistence.Database.Postgres.Migrations }); migrationBuilder.CreateTable( - name: "data_saub", + name: "data_scheme", columns: table => new { - date = table.Column(type: "timestamp with time zone", nullable: false), - mode = table.Column(type: "integer", nullable: true), - user = table.Column(type: "text", nullable: true), - wellDepth = table.Column(type: "double precision", nullable: true), - bitDepth = table.Column(type: "double precision", nullable: true), - blockPosition = table.Column(type: "double precision", nullable: true), - blockSpeed = table.Column(type: "double precision", nullable: true), - pressure = table.Column(type: "double precision", nullable: true), - axialLoad = table.Column(type: "double precision", nullable: true), - hookWeight = table.Column(type: "double precision", nullable: true), - rotorTorque = table.Column(type: "double precision", nullable: true), - rotorSpeed = table.Column(type: "double precision", nullable: true), - flow = table.Column(type: "double precision", nullable: true), - mseState = table.Column(type: "smallint", nullable: false), - idFeedRegulator = table.Column(type: "integer", nullable: false), - mse = table.Column(type: "double precision", nullable: true), - pump0Flow = table.Column(type: "double precision", nullable: true), - pump1Flow = table.Column(type: "double precision", nullable: true), - pump2Flow = table.Column(type: "double precision", nullable: true) + DiscriminatorId = table.Column(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"), + PropNames = table.Column(type: "jsonb", nullable: false, comment: "Наименования полей в порядке индексации") }, constraints: table => { - table.PrimaryKey("PK_data_saub", x => x.date); + table.PrimaryKey("PK_data_scheme", x => x.DiscriminatorId); }); migrationBuilder.CreateTable( @@ -93,28 +77,33 @@ namespace DD.Persistence.Database.Postgres.Migrations columns: table => new { Key = table.Column(type: "uuid", nullable: false, comment: "Ключ"), - Created = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата создания уставки"), - Value = table.Column(type: "jsonb", nullable: false, comment: "Значение уставки"), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Дата создания уставки"), + Value = table.Column(type: "jsonb", nullable: false, comment: "Значение уставки"), IdUser = table.Column(type: "uuid", nullable: false, comment: "Id автора последнего изменения") }, constraints: table => { - table.PrimaryKey("PK_setpoint", x => new { x.Key, x.Created }); + table.PrimaryKey("PK_setpoint", x => new { x.Key, x.Timestamp }); }); migrationBuilder.CreateTable( - name: "timestamped_set", + name: "timestamped_values", columns: table => new { - IdDiscriminator = table.Column(type: "uuid", nullable: false, comment: "Дискриминатор ссылка на тип сохраняемых данных"), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Отметка времени, строго в UTC"), - Set = table.Column(type: "jsonb", nullable: false, comment: "Набор сохраняемых данных") + Timestamp = table.Column(type: "timestamp with time zone", nullable: false, comment: "Временная отметка"), + DiscriminatorId = table.Column(type: "uuid", nullable: false, comment: "Дискриминатор системы"), + Values = table.Column(type: "jsonb", nullable: false, comment: "Данные") }, constraints: table => { - table.PrimaryKey("PK_timestamped_set", x => new { x.IdDiscriminator, x.Timestamp }); - }, - comment: "Общая таблица данных временных рядов"); + table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp }); + table.ForeignKey( + name: "FK_timestamped_values_data_scheme_DiscriminatorId", + column: x => x.DiscriminatorId, + principalTable: "data_scheme", + principalColumn: "DiscriminatorId", + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateTable( name: "tech_message", @@ -153,9 +142,6 @@ namespace DD.Persistence.Database.Postgres.Migrations migrationBuilder.DropTable( name: "change_log"); - migrationBuilder.DropTable( - name: "data_saub"); - migrationBuilder.DropTable( name: "parameter_data"); @@ -166,10 +152,13 @@ namespace DD.Persistence.Database.Postgres.Migrations name: "tech_message"); migrationBuilder.DropTable( - name: "timestamped_set"); + name: "timestamped_values"); migrationBuilder.DropTable( name: "data_source_system"); + + migrationBuilder.DropTable( + name: "data_scheme"); } } } diff --git a/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs b/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs index 467f3a6..3db9dda 100644 --- a/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs +++ b/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Text.Json; using DD.Persistence.Database.Model; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -22,6 +23,23 @@ namespace DD.Persistence.Database.Postgres.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b => + { + b.Property("DiscriminatorId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasComment("Идентификатор схемы данных"); + + b.Property("PropNames") + .IsRequired() + .HasColumnType("jsonb") + .HasComment("Наименования полей в порядке индексации"); + + b.HasKey("DiscriminatorId"); + + b.ToTable("data_scheme"); + }); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => { b.Property("SystemId") @@ -102,27 +120,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("tech_message"); }); - modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedSet", b => + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => { - b.Property("IdDiscriminator") + b.Property("DiscriminatorId") .HasColumnType("uuid") - .HasComment("Дискриминатор ссылка на тип сохраняемых данных"); + .HasComment("Дискриминатор системы"); b.Property("Timestamp") .HasColumnType("timestamp with time zone") - .HasComment("Отметка времени, строго в UTC"); + .HasComment("Временная отметка"); - b.Property("Set") + b.Property("Values") .IsRequired() .HasColumnType("jsonb") - .HasComment("Набор сохраняемых данных"); + .HasComment("Данные"); - b.HasKey("IdDiscriminator", "Timestamp"); + b.HasKey("DiscriminatorId", "Timestamp"); - b.ToTable("timestamped_set", t => - { - t.HasComment("Общая таблица данных временных рядов"); - }); + b.ToTable("timestamped_values"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -178,96 +193,13 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("change_log"); }); - modelBuilder.Entity("DD.Persistence.Database.Model.DataSaub", b => - { - b.Property("Date") - .HasColumnType("timestamp with time zone") - .HasColumnName("date"); - - b.Property("AxialLoad") - .HasColumnType("double precision") - .HasColumnName("axialLoad"); - - b.Property("BitDepth") - .HasColumnType("double precision") - .HasColumnName("bitDepth"); - - b.Property("BlockPosition") - .HasColumnType("double precision") - .HasColumnName("blockPosition"); - - b.Property("BlockSpeed") - .HasColumnType("double precision") - .HasColumnName("blockSpeed"); - - b.Property("Flow") - .HasColumnType("double precision") - .HasColumnName("flow"); - - b.Property("HookWeight") - .HasColumnType("double precision") - .HasColumnName("hookWeight"); - - b.Property("IdFeedRegulator") - .HasColumnType("integer") - .HasColumnName("idFeedRegulator"); - - b.Property("Mode") - .HasColumnType("integer") - .HasColumnName("mode"); - - b.Property("Mse") - .HasColumnType("double precision") - .HasColumnName("mse"); - - b.Property("MseState") - .HasColumnType("smallint") - .HasColumnName("mseState"); - - b.Property("Pressure") - .HasColumnType("double precision") - .HasColumnName("pressure"); - - b.Property("Pump0Flow") - .HasColumnType("double precision") - .HasColumnName("pump0Flow"); - - b.Property("Pump1Flow") - .HasColumnType("double precision") - .HasColumnName("pump1Flow"); - - b.Property("Pump2Flow") - .HasColumnType("double precision") - .HasColumnName("pump2Flow"); - - b.Property("RotorSpeed") - .HasColumnType("double precision") - .HasColumnName("rotorSpeed"); - - b.Property("RotorTorque") - .HasColumnType("double precision") - .HasColumnName("rotorTorque"); - - b.Property("User") - .HasColumnType("text") - .HasColumnName("user"); - - b.Property("WellDepth") - .HasColumnType("double precision") - .HasColumnName("wellDepth"); - - b.HasKey("Date"); - - b.ToTable("data_saub"); - }); - modelBuilder.Entity("DD.Persistence.Database.Model.Setpoint", b => { b.Property("Key") .HasColumnType("uuid") .HasComment("Ключ"); - b.Property("Created") + b.Property("Timestamp") .HasColumnType("timestamp with time zone") .HasComment("Дата создания уставки"); @@ -275,12 +207,11 @@ namespace DD.Persistence.Database.Postgres.Migrations .HasColumnType("uuid") .HasComment("Id автора последнего изменения"); - b.Property("Value") - .IsRequired() + b.Property("Value") .HasColumnType("jsonb") .HasComment("Значение уставки"); - b.HasKey("Key", "Created"); + b.HasKey("Key", "Timestamp"); b.ToTable("setpoint"); }); @@ -295,6 +226,17 @@ namespace DD.Persistence.Database.Postgres.Migrations b.Navigation("System"); }); + + modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => + { + b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme") + .WithMany() + .HasForeignKey("DiscriminatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataScheme"); + }); #pragma warning restore 612, 618 } } diff --git a/DD.Persistence.Database/Entity/ChangeLog.cs b/DD.Persistence.Database/Entity/ChangeLog.cs index a5254ae..1fb37d8 100644 --- a/DD.Persistence.Database/Entity/ChangeLog.cs +++ b/DD.Persistence.Database/Entity/ChangeLog.cs @@ -1,8 +1,9 @@  using Microsoft.EntityFrameworkCore; -using DD.Persistence.Models; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using DD.Persistence.ModelsAbstractions; +using DD.Persistence.Database.EntityAbstractions; namespace DD.Persistence.Database.Model; diff --git a/DD.Persistence.Database/Entity/DataSaub.cs b/DD.Persistence.Database/Entity/DataSaub.cs deleted file mode 100644 index 0442fc4..0000000 --- a/DD.Persistence.Database/Entity/DataSaub.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DD.Persistence.Database.Model; - -[Table("data_saub")] -public class DataSaub : ITimestampedData -{ - [Key, Column("date")] - public DateTimeOffset Date { get; set; } - - [Column("mode")] - public int? Mode { get; set; } - - [Column("user")] - public string? User { get; set; } - - [Column("wellDepth")] - public double? WellDepth { get; set; } - - [Column("bitDepth")] - public double? BitDepth { get; set; } - - [Column("blockPosition")] - public double? BlockPosition { get; set; } - - [Column("blockSpeed")] - public double? BlockSpeed { get; set; } - - [Column("pressure")] - public double? Pressure { get; set; } - - [Column("axialLoad")] - public double? AxialLoad { get; set; } - - [Column("hookWeight")] - public double? HookWeight { get; set; } - - [Column("rotorTorque")] - public double? RotorTorque { get; set; } - - [Column("rotorSpeed")] - public double? RotorSpeed { get; set; } - - [Column("flow")] - public double? Flow { get; set; } - - [Column("mseState")] - public short MseState { get; set; } - - [Column("idFeedRegulator")] - public int IdFeedRegulator { get; set; } - - [Column("mse")] - public double? Mse { get; set; } - - [Column("pump0Flow")] - public double? Pump0Flow { get; set; } - - [Column("pump1Flow")] - public double? Pump1Flow { get; set; } - - [Column("pump2Flow")] - public double? Pump2Flow { get; set; } -} diff --git a/DD.Persistence.Database/Entity/DataScheme.cs b/DD.Persistence.Database/Entity/DataScheme.cs new file mode 100644 index 0000000..75794c0 --- /dev/null +++ b/DD.Persistence.Database/Entity/DataScheme.cs @@ -0,0 +1,15 @@ +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; } = []; +} diff --git a/DD.Persistence.Database/Entity/DataSourceSystem.cs b/DD.Persistence.Database/Entity/DataSourceSystem.cs index b11e38e..cc72b91 100644 --- a/DD.Persistence.Database/Entity/DataSourceSystem.cs +++ b/DD.Persistence.Database/Entity/DataSourceSystem.cs @@ -11,7 +11,7 @@ public class DataSourceSystem public Guid SystemId { get; set; } [Required, Column(TypeName = "varchar(256)"), Comment("Наименование системы - источника данных")] - public required string Name { get; set; } + public string Name { get; set; } = string.Empty; [Comment("Описание системы - источника данных")] public string? Description { get; set; } diff --git a/DD.Persistence.Database/Entity/ITimestampedData.cs b/DD.Persistence.Database/Entity/ITimestampedData.cs deleted file mode 100644 index ce5468c..0000000 --- a/DD.Persistence.Database/Entity/ITimestampedData.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DD.Persistence.Database.Model; -public interface ITimestampedData -{ - /// - /// Дата (должна быть обязательно в UTC) - /// - DateTimeOffset Date { get; set; } -} diff --git a/DD.Persistence.Database/Entity/ParameterData.cs b/DD.Persistence.Database/Entity/ParameterData.cs index 32ac5a4..1b2fb37 100644 --- a/DD.Persistence.Database/Entity/ParameterData.cs +++ b/DD.Persistence.Database/Entity/ParameterData.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using DD.Persistence.Database.EntityAbstractions; +using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -6,7 +7,7 @@ namespace DD.Persistence.Database.Entity; [Table("parameter_data")] [PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))] -public class ParameterData +public class ParameterData : ITimestampedItem { [Required, Comment("Дискриминатор системы")] public Guid DiscriminatorId { get; set; } diff --git a/DD.Persistence.Database/Entity/Setpoint.cs b/DD.Persistence.Database/Entity/Setpoint.cs index 474fb28..1175dc5 100644 --- a/DD.Persistence.Database/Entity/Setpoint.cs +++ b/DD.Persistence.Database/Entity/Setpoint.cs @@ -1,20 +1,22 @@ -using Microsoft.EntityFrameworkCore; +using DD.Persistence.Database.EntityAbstractions; +using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json; namespace DD.Persistence.Database.Model { [Table("setpoint")] - [PrimaryKey(nameof(Key), nameof(Created))] - public class Setpoint + [PrimaryKey(nameof(Key), nameof(Timestamp))] + public class Setpoint : ITimestampedItem { [Comment("Ключ")] public Guid Key { get; set; } [Column(TypeName = "jsonb"), Comment("Значение уставки")] - public required object Value { get; set; } + public required JsonElement Value { get; set; } [Comment("Дата создания уставки")] - public DateTimeOffset Created { get; set; } + public DateTimeOffset Timestamp { get; set; } [Comment("Id автора последнего изменения")] public Guid IdUser { get; set; } diff --git a/DD.Persistence.Database/Entity/TechMessage.cs b/DD.Persistence.Database/Entity/TechMessage.cs index 4c7733b..c57cd3b 100644 --- a/DD.Persistence.Database/Entity/TechMessage.cs +++ b/DD.Persistence.Database/Entity/TechMessage.cs @@ -1,3 +1,4 @@ +using DD.Persistence.Database.EntityAbstractions; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -5,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace DD.Persistence.Database.Entity { [Table("tech_message")] - public class TechMessage + public class TechMessage : ITimestampedItem { [Key, Comment("Id события")] public Guid EventId { get; set; } diff --git a/DD.Persistence.Database/Entity/TimestampedSet.cs b/DD.Persistence.Database/Entity/TimestampedSet.cs deleted file mode 100644 index 10ffd2c..0000000 --- a/DD.Persistence.Database/Entity/TimestampedSet.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DD.Persistence.Database.Entity; - -[Table("timestamped_set")] -[Comment("Общая таблица данных временных рядов")] -[PrimaryKey(nameof(IdDiscriminator), nameof(Timestamp))] -public record TimestampedSet( - [property: Comment("Дискриминатор ссылка на тип сохраняемых данных")] Guid IdDiscriminator, - [property: Comment("Отметка времени, строго в UTC")] DateTimeOffset Timestamp, - [property: Column(TypeName = "jsonb"), Comment("Набор сохраняемых данных")] IDictionary Set); diff --git a/DD.Persistence.Database/Entity/TimestampedValues.cs b/DD.Persistence.Database/Entity/TimestampedValues.cs new file mode 100644 index 0000000..a2835f7 --- /dev/null +++ b/DD.Persistence.Database/Entity/TimestampedValues.cs @@ -0,0 +1,23 @@ +using DD.Persistence.Database.EntityAbstractions; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace DD.Persistence.Database.Entity; + +[Table("timestamped_values")] +[PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))] +public class TimestampedValues : ITimestampedItem +{ + [Comment("Временная отметка"), Key] + public DateTimeOffset Timestamp { get; set; } + + [Comment("Дискриминатор системы"),] + public Guid DiscriminatorId { get; set; } + + [Comment("Данные"), Column(TypeName = "jsonb")] + public required object[] Values { get; set; } + + [Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")] + public virtual DataScheme? DataScheme { get; set; } +} diff --git a/DD.Persistence.Database/Entity/IChangeLog.cs b/DD.Persistence.Database/EntityAbstractions/IChangeLog.cs similarity index 95% rename from DD.Persistence.Database/Entity/IChangeLog.cs rename to DD.Persistence.Database/EntityAbstractions/IChangeLog.cs index b91808b..4d082a7 100644 --- a/DD.Persistence.Database/Entity/IChangeLog.cs +++ b/DD.Persistence.Database/EntityAbstractions/IChangeLog.cs @@ -1,5 +1,4 @@ - -namespace DD.Persistence.Database.Model; +namespace DD.Persistence.Database.EntityAbstractions; /// /// Часть записи, описывающая изменение diff --git a/DD.Persistence.Database/EntityAbstractions/ITimestampedItem.cs b/DD.Persistence.Database/EntityAbstractions/ITimestampedItem.cs new file mode 100644 index 0000000..f505796 --- /dev/null +++ b/DD.Persistence.Database/EntityAbstractions/ITimestampedItem.cs @@ -0,0 +1,8 @@ +namespace DD.Persistence.Database.EntityAbstractions; +public interface ITimestampedItem +{ + /// + /// Дата (должна быть обязательно в UTC) + /// + DateTimeOffset Timestamp { get; set; } +} diff --git a/DD.Persistence.Database/PersistenceDbContext.cs b/DD.Persistence.Database/PersistenceDbContext.cs index a0587cd..5a9a13c 100644 --- a/DD.Persistence.Database/PersistenceDbContext.cs +++ b/DD.Persistence.Database/PersistenceDbContext.cs @@ -9,11 +9,11 @@ namespace DD.Persistence.Database; /// public class PersistenceDbContext : DbContext { - public DbSet DataSaub => Set(); - public DbSet Setpoint => Set(); - public DbSet TimestampedSets => Set(); + public DbSet DataSchemes => Set(); + + public DbSet TimestampedValues => Set(); public DbSet ChangeLog => Set(); @@ -31,8 +31,12 @@ public class PersistenceDbContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity() - .Property(e => e.Set) + modelBuilder.Entity() + .Property(e => e.PropNames) + .HasJsonConversion(); + + modelBuilder.Entity() + .Property(e => e.Values) .HasJsonConversion(); modelBuilder.Entity() diff --git a/DD.Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs index 3f1bb6c..64419e7 100644 --- a/DD.Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/ChangeLogControllerTest.cs @@ -7,6 +7,11 @@ using DD.Persistence.Models.Requests; using Xunit; using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client; +using DD.Persistence.Client.Clients.Interfaces.Refit; +using DD.Persistence.Client.Clients; +using Microsoft.Extensions.Logging; +using Refit; +using System.Net.Http; namespace DD.Persistence.IntegrationTests.Controllers; public class ChangeLogControllerTest : BaseIntegrationTest @@ -16,10 +21,12 @@ public class ChangeLogControllerTest : BaseIntegrationTest public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory) { - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); - client = persistenceClientFactory.GetChangeLogClient(); + client = scope.ServiceProvider + .GetRequiredService(); } [Fact] diff --git a/DD.Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs deleted file mode 100644 index 7daad50..0000000 --- a/DD.Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -using DD.Persistence.Database.Model; -using DD.Persistence.Models; -using Xunit; - -namespace DD.Persistence.IntegrationTests.Controllers; -public class DataSaubControllerTest : TimeSeriesBaseControllerTest -{ - private readonly DataSaubDto dto = new() - { - AxialLoad = 1, - BitDepth = 2, - BlockPosition = 3, - BlockSpeed = 4, - Date = DateTimeOffset.UtcNow, - Flow = 5, - HookWeight = 6, - IdFeedRegulator = 8, - Mode = 9, - Mse = 10, - MseState = 11, - Pressure = 12, - Pump0Flow = 13, - Pump1Flow = 14, - Pump2Flow = 15, - RotorSpeed = 16, - RotorTorque = 17, - User = string.Empty, - WellDepth = 18, - }; - - private readonly DataSaub entity = new() - { - AxialLoad = 1, - BitDepth = 2, - BlockPosition = 3, - BlockSpeed = 4, - Date = DateTimeOffset.UtcNow, - Flow = 5, - HookWeight = 6, - IdFeedRegulator = 8, - Mode = 9, - Mse = 10, - MseState = 11, - Pressure = 12, - Pump0Flow = 13, - Pump1Flow = 14, - Pump2Flow = 15, - RotorSpeed = 16, - RotorTorque = 17, - User = string.Empty, - WellDepth = 18, - }; - - public DataSaubControllerTest(WebAppFactoryFixture factory) : base(factory) - { - } - - [Fact] - public async Task InsertRange_returns_success() - { - await InsertRangeSuccess(dto); - } - - [Fact] - public async Task Get_returns_success() - { - var beginDate = DateTimeOffset.UtcNow.AddDays(-1); - var endDate = DateTimeOffset.UtcNow; - await GetSuccess(beginDate, endDate, entity); - } - - [Fact] - public async Task GetDatesRange_returns_success() - { - await GetDatesRangeSuccess(entity); - } - - - - [Fact] - public async Task GetResampledData_returns_success() - { - await GetResampledDataSuccess(entity); - } -} diff --git a/DD.Persistence.IntegrationTests/Controllers/DataSourceSystemControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/DataSourceSystemControllerTest.cs index 03d5b8a..853a549 100644 --- a/DD.Persistence.IntegrationTests/Controllers/DataSourceSystemControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/DataSourceSystemControllerTest.cs @@ -6,6 +6,8 @@ using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Database.Entity; using DD.Persistence.Models; using Xunit; +using DD.Persistence.Client.Clients.Interfaces.Refit; +using Microsoft.Extensions.Logging; namespace DD.Persistence.IntegrationTests.Controllers { @@ -16,11 +18,12 @@ namespace DD.Persistence.IntegrationTests.Controllers private readonly IMemoryCache memoryCache; public DataSourceSystemControllerTest(WebAppFactoryFixture factory) : base(factory) { - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); - dataSourceSystemClient = persistenceClientFactory.GetDataSourceSystemClient(); + dataSourceSystemClient = scope.ServiceProvider + .GetRequiredService(); memoryCache = scope.ServiceProvider.GetRequiredService(); } diff --git a/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index d79b6c0..e84402a 100644 --- a/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,8 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; 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.Database.Model; -using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Text.Json; using Xunit; namespace DD.Persistence.IntegrationTests.Controllers @@ -10,20 +13,45 @@ namespace DD.Persistence.IntegrationTests.Controllers public class SetpointControllerTest : BaseIntegrationTest { private readonly ISetpointClient setpointClient; - private class TestObject - { - public string? Value1 { get; set; } - public int? Value2 { get; set; } - } + private readonly SetpointConfigStorage configStorage; public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); - setpointClient = persistenceClientFactory.GetSetpointClient(); + setpointClient = scope.ServiceProvider + .GetRequiredService(); + + configStorage = (SetpointConfigStorage)scope.ServiceProvider.GetRequiredService(); } + + [Fact] + public async Task GetCurrent_returns_correctType() + { + var id = Guid.Parse("e0fcad22-1761-476e-a729-a3c59d51ba41"); + + configStorage.AddOrReplace(id, typeof(float)); + + await setpointClient.Add(id, 48.3f, CancellationToken.None); + + //act + var response = await setpointClient.GetCurrent([id], CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + Assert.Single(response); + var item = response.First(); + Assert.Equal(item.Key, id); + + Assert.IsNotType(item.Value); + Assert.Equal(item.Value, 48.3f); + } + + + [Fact] public async Task GetCurrent_returns_success() { @@ -35,7 +63,7 @@ namespace DD.Persistence.IntegrationTests.Controllers }; //act - var response = await setpointClient.GetCurrent(setpointKeys, new CancellationToken()); + var response = await setpointClient.GetCurrent(setpointKeys, CancellationToken.None); //assert Assert.NotNull(response); @@ -149,7 +177,7 @@ namespace DD.Persistence.IntegrationTests.Controllers await Add(); - var dateBegin = DateTimeOffset.MinValue; + var dateBegin = DateTimeOffset.UtcNow.AddDays(-1); var take = 1; var part = await setpointClient.GetPart(dateBegin, take, CancellationToken.None); @@ -160,14 +188,17 @@ namespace DD.Persistence.IntegrationTests.Controllers Assert.NotNull(response); var expectedValue = part! - .FirstOrDefault()!.Created - .ToString("dd.MM.yyyy-HH:mm:ss"); + .FirstOrDefault()!.Timestamp + .ToUniversalTime() + .ToString(); var actualValueFrom = response.From - .ToString("dd.MM.yyyy-HH:mm:ss"); + .ToUniversalTime() + .ToString(); Assert.Equal(expectedValue, actualValueFrom); var actualValueTo = response.To - .ToString("dd.MM.yyyy-HH:mm:ss"); + .ToUniversalTime() + .ToString(); Assert.Equal(expectedValue, actualValueTo); } @@ -212,7 +243,7 @@ namespace DD.Persistence.IntegrationTests.Controllers { //arrange var setpointKey = Guid.NewGuid(); - var setpointValue = new TestObject() + var setpointValue = new { Value1 = "1", Value2 = 2 diff --git a/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs index bdcddb9..7974470 100644 --- a/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs @@ -1,38 +1,41 @@ -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using DD.Persistence.Client; +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.Database.Entity; using DD.Persistence.Models; using DD.Persistence.Models.Enumerations; using DD.Persistence.Models.Requests; -using System.Net; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Xunit; namespace DD.Persistence.IntegrationTests.Controllers { - public class TechMessagesControllerTest : BaseIntegrationTest - { - private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DataSourceSystem).FullName}CacheKey"; - private readonly ITechMessagesClient techMessagesClient; - private readonly IMemoryCache memoryCache; - public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) - { - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + public class TechMessagesControllerTest : BaseIntegrationTest + { + private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey"; + private readonly ITechMessagesClient techMessagesClient; + private readonly IMemoryCache memoryCache; + public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); - techMessagesClient = persistenceClientFactory.GetTechMessagesClient(); - memoryCache = scope.ServiceProvider.GetRequiredService(); - } + techMessagesClient = scope.ServiceProvider + .GetRequiredService(); + memoryCache = scope.ServiceProvider.GetRequiredService(); + } - [Fact] - public async Task GetPage_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + [Fact] + public async Task GetPage_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); var requestDto = new PaginationRequest() { @@ -44,235 +47,235 @@ namespace DD.Persistence.IntegrationTests.Controllers //act var response = await techMessagesClient.GetPage(requestDto, CancellationToken.None); - //assert - Assert.NotNull(response); - Assert.Empty(response.Items); - Assert.Equal(requestDto.Skip, response.Skip); - Assert.Equal(requestDto.Take, response.Take); - } + //assert + Assert.NotNull(response); + Assert.Empty(response.Items); + Assert.Equal(requestDto.Skip, response.Skip); + Assert.Equal(requestDto.Take, response.Take); + } - [Fact] - public async Task GetPage_AfterSave_returns_success() - { - //arrange - var dtos = await InsertRange(Guid.NewGuid()); - var dtosCount = dtos.Count(); - var requestDto = new PaginationRequest() - { - Skip = 0, - Take = 2, - SortSettings = nameof(TechMessage.CategoryId) - }; + [Fact] + public async Task GetPage_AfterSave_returns_success() + { + //arrange + var dtos = await InsertRange(Guid.NewGuid()); + var dtosCount = dtos.Count(); + var requestDto = new PaginationRequest() + { + Skip = 0, + Take = 2, + SortSettings = nameof(TechMessage.CategoryId) + }; //act var response = await techMessagesClient.GetPage(requestDto, CancellationToken.None); - //assert - Assert.NotNull(response); - Assert.Equal(dtosCount, response.Count); - } - - [Fact] - public async Task InsertRange_returns_success() - { - await InsertRange(Guid.NewGuid()); - } - - [Fact] - public async Task InsertRange_returns_BadRequest() - { - //arrange - const string exceptionMessage = "Ошибка валидации, формата или маршрутизации запроса"; - var systemId = Guid.NewGuid(); - var dtos = new List() - { - new TechMessageDto() - { - EventId = Guid.NewGuid(), - CategoryId = -1, // < 0 - Timestamp = DateTimeOffset.UtcNow, - Text = string.Empty, // length < 0 - EventState = EventState.Triggered - } - }; - - try - { - //act - var response = await techMessagesClient.AddRange(systemId, dtos, CancellationToken.None); - } - catch (Exception ex) - { - //assert - Assert.Equal(exceptionMessage, ex.Message); - } - } - - [Fact] - public async Task GetSystems_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); - - //act - var response = await techMessagesClient.GetSystems(CancellationToken.None); - - //assert - Assert.NotNull(response); - Assert.Empty(response); - } - - [Fact] - public async Task GetSystems_AfterSave_returns_success() - { - //arrange - await InsertRange(Guid.NewGuid()); - - //act - var response = await techMessagesClient.GetSystems(CancellationToken.None); - - //assert - Assert.NotNull(response); - var expectedSystemCount = 1; - Assert.Equal(expectedSystemCount, response!.Count()); - } - - [Fact] - public async Task GetStatistics_returns_success() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); - - var categoryIds = new [] { 1, 2 }; - var systemIds = new [] { Guid.NewGuid() }; - - //act - var response = await techMessagesClient.GetStatistics(systemIds, categoryIds, CancellationToken.None); - - //assert - Assert.NotNull(response); - Assert.Empty(response); - } - - [Fact] - public async Task GetStatistics_AfterSave_returns_success() - { - //arrange - var categoryIds = new[] { 1 }; - var systemId = Guid.NewGuid(); - var dtos = await InsertRange(systemId); - var filteredDtos = dtos.Where(e => categoryIds.Contains(e.CategoryId)); - - //act - var response = await techMessagesClient.GetStatistics([systemId], categoryIds, CancellationToken.None); - - //assert - Assert.NotNull(response); - Assert.NotEmpty(response); - var categories = response - .FirstOrDefault()!.Categories - .Count(); - Assert.Equal(filteredDtos.Count(), categories); - } - - [Fact] - public async Task GetDatesRange_returns_NoContent() - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); - - //act - var response = await techMessagesClient.GetDatesRangeAsync(CancellationToken.None); - //assert - Assert.Null(response); - } + Assert.NotNull(response); + Assert.Equal(dtosCount, response.Count); + } - [Fact] - public async Task GetDatesRange_AfterSave_returns_success() - { - //arrange - await InsertRange(Guid.NewGuid()); + [Fact] + public async Task InsertRange_returns_success() + { + await InsertRange(Guid.NewGuid()); + } + + [Fact] + public async Task InsertRange_returns_BadRequest() + { + //arrange + const string exceptionMessage = "Ошибка валидации, формата или маршрутизации запроса"; + var systemId = Guid.NewGuid(); + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = -1, // < 0 + Timestamp = DateTimeOffset.UtcNow, + Text = string.Empty, // length < 0 + EventState = EventState.Triggered + } + }; + + try + { + //act + var response = await techMessagesClient.AddRange(systemId, dtos, CancellationToken.None); + } + catch (Exception ex) + { + //assert + Assert.Equal(exceptionMessage, ex.Message); + } + } + + [Fact] + public async Task GetSystems_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + //act + var response = await techMessagesClient.GetSystems(CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.Empty(response); + } + + [Fact] + public async Task GetSystems_AfterSave_returns_success() + { + //arrange + await InsertRange(Guid.NewGuid()); + + //act + var response = await techMessagesClient.GetSystems(CancellationToken.None); + + //assert + Assert.NotNull(response); + var expectedSystemCount = 1; + Assert.Equal(expectedSystemCount, response!.Count()); + } + + [Fact] + public async Task GetStatistics_returns_success() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + + var categoryIds = new[] { 1, 2 }; + var systemIds = new[] { Guid.NewGuid() }; + + //act + var response = await techMessagesClient.GetStatistics(systemIds, categoryIds, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.Empty(response); + } + + [Fact] + public async Task GetStatistics_AfterSave_returns_success() + { + //arrange + var categoryIds = new[] { 1 }; + var systemId = Guid.NewGuid(); + var dtos = await InsertRange(systemId); + var filteredDtos = dtos.Where(e => categoryIds.Contains(e.CategoryId)); + + //act + var response = await techMessagesClient.GetStatistics([systemId], categoryIds, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + var categories = response + .FirstOrDefault()!.Categories + .Count(); + Assert.Equal(filteredDtos.Count(), categories); + } + + [Fact] + public async Task GetDatesRange_returns_NoContent() + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); //act var response = await techMessagesClient.GetDatesRangeAsync(CancellationToken.None); - //assert - Assert.NotNull(response); - Assert.NotNull(response?.From); - Assert.NotNull(response?.To); - } + //assert + Assert.Null(response); + } - // [Fact] - // public async Task GetPart_returns_success() - // { - // //arrange - // var dateBegin = DateTimeOffset.UtcNow; - // var take = 2; + [Fact] + public async Task GetDatesRange_AfterSave_returns_success() + { + //arrange + await InsertRange(Guid.NewGuid()); - // //act - // var response = await techMessagesClient.GetPart(dateBegin, take, CancellationToken.None); + //act + var response = await techMessagesClient.GetDatesRangeAsync(CancellationToken.None); - // //assert - // Assert.NotNull(response); - // Assert.Empty(response); - //} + //assert + Assert.NotNull(response); + Assert.NotNull(response?.From); + Assert.NotNull(response?.To); + } - [Fact] - public async Task GetPart_AfterSave_returns_success() - { - //arrange - var dateBegin = DateTimeOffset.UtcNow; - var take = 1; - await InsertRange(Guid.NewGuid()); + // [Fact] + // public async Task GetPart_returns_success() + // { + // //arrange + // var dateBegin = DateTimeOffset.UtcNow; + // var take = 2; + + // //act + // var response = await techMessagesClient.GetPart(dateBegin, take, CancellationToken.None); + + // //assert + // Assert.NotNull(response); + // Assert.Empty(response); + //} + + [Fact] + public async Task GetPart_AfterSave_returns_success() + { + //arrange + var dateBegin = DateTimeOffset.UtcNow; + var take = 1; + await InsertRange(Guid.NewGuid()); //act var response = await techMessagesClient.GetPart(dateBegin, take, CancellationToken.None); - //assert - Assert.NotNull(response); - Assert.NotEmpty(response); - } + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + } - private async Task> InsertRange(Guid systemId) - { - //arrange - memoryCache.Remove(SystemCacheKey); - dbContext.CleanupDbSet(); - dbContext.CleanupDbSet(); + private async Task> InsertRange(Guid systemId) + { + //arrange + memoryCache.Remove(SystemCacheKey); + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); - var dtos = new List() - { - new TechMessageDto() - { - EventId = Guid.NewGuid(), - CategoryId = 1, - Timestamp = DateTimeOffset.UtcNow, - Text = nameof(TechMessageDto.Text), - EventState = Models.Enumerations.EventState.Triggered - }, - new TechMessageDto() - { - EventId = Guid.NewGuid(), - CategoryId = 2, - Timestamp = DateTimeOffset.UtcNow, - Text = nameof(TechMessageDto.Text), - EventState = Models.Enumerations.EventState.Triggered - } - }; + var dtos = new List() + { + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 1, + Timestamp = DateTimeOffset.UtcNow, + Text = nameof(TechMessageDto.Text), + EventState = Models.Enumerations.EventState.Triggered + }, + new TechMessageDto() + { + EventId = Guid.NewGuid(), + CategoryId = 2, + Timestamp = DateTimeOffset.UtcNow, + Text = nameof(TechMessageDto.Text), + EventState = Models.Enumerations.EventState.Triggered + } + }; - //act - var response = await techMessagesClient.AddRange(systemId, dtos, CancellationToken.None); + //act + var response = await techMessagesClient.AddRange(systemId, dtos, CancellationToken.None); - //assert - Assert.Equal(dtos.Count, response); + //assert + Assert.Equal(dtos.Count, response); return dtos; } diff --git a/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs deleted file mode 100644 index 7d75875..0000000 --- a/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Mapster; -using Microsoft.Extensions.DependencyInjection; -using DD.Persistence.Client; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Database.Model; -using System.Net; -using Xunit; - -namespace DD.Persistence.IntegrationTests.Controllers; - -public abstract class TimeSeriesBaseControllerTest : BaseIntegrationTest - where TEntity : class, ITimestampedData, new() - where TDto : class, new() -{ - private readonly ITimeSeriesClient timeSeriesClient; - - public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory) - { - dbContext.CleanupDbSet(); - - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); - - timeSeriesClient = persistenceClientFactory.GetTimeSeriesClient(); - } - - public async Task InsertRangeSuccess(TDto dto) - { - //arrange - var expected = dto.Adapt(); - - //act - var response = await timeSeriesClient.AddRange(new TDto[] { expected }, new CancellationToken()); - - //assert - Assert.Equal(1, response); - } - - public async Task GetSuccess(DateTimeOffset beginDate, DateTimeOffset endDate, TEntity entity) - { - //arrange - var dbset = dbContext.Set(); - - dbset.Add(entity); - - dbContext.SaveChanges(); - - var response = await timeSeriesClient.Get(beginDate, endDate, new CancellationToken()); - - //assert - Assert.NotNull(response); - Assert.Single(response); - } - - public async Task GetDatesRangeSuccess(TEntity entity) - { - //arrange - var datesRangeExpected = 30; - - var entity2 = entity.Adapt(); - entity2.Date = entity.Date.AddDays(datesRangeExpected); - - var dbset = dbContext.Set(); - dbset.Add(entity); - dbset.Add(entity2); - - dbContext.SaveChanges(); - - var response = await timeSeriesClient.GetDatesRange(new CancellationToken()); - - //assert - Assert.NotNull(response); - - var datesRangeActual = (response.To - response.From).Days; - Assert.Equal(datesRangeExpected, datesRangeActual); - } - - public async Task GetResampledDataSuccess(TEntity entity) - { - //arrange - var approxPointsCount = 10; - var differenceBetweenStartAndEndDays = 50; - - var entities = new List(); - for (var i = 1; i <= differenceBetweenStartAndEndDays; i++) - { - var entity2 = entity.Adapt(); - entity2.Date = entity.Date.AddDays(i - 1); - - entities.Add(entity2); - } - - var dbset = dbContext.Set(); - dbset.AddRange(entities); - - dbContext.SaveChanges(); - - var response = await timeSeriesClient.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount, new CancellationToken()); - - //assert - Assert.NotNull(response); - - var ratio = entities.Count / approxPointsCount; - if (ratio > 1) - { - var expectedResampledCount = entities - .Where((_, index) => index % ratio == 0) - .Count(); - - Assert.Equal(expectedResampledCount, response.Count()); - } - else - { - Assert.Equal(entities.Count(), response.Count()); - } - } -} diff --git a/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs deleted file mode 100644 index 567b8c8..0000000 --- a/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using DD.Persistence.Client; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Models; -using Xunit; - -namespace DD.Persistence.IntegrationTests.Controllers; -public class TimestampedSetControllerTest : BaseIntegrationTest -{ - private readonly ITimestampedSetClient client; - - public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory) - { - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); - - client = persistenceClientFactory.GetTimestampedSetClient(); - } - - [Fact] - public async Task InsertRange() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - IEnumerable testSets = Generate(10, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - - // act - var response = await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - - // assert - Assert.Equal(testSets.Count(), response); - } - - [Fact] - public async Task Get_without_filter() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - - // act - var response = await client.Get(idDiscriminator, null, null, 0, int.MaxValue, CancellationToken.None); - - // assert - Assert.NotNull(response); - Assert.Equal(count, response.Count()); - } - - [Fact] - public async Task Get_with_filter_props() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - string[] props = ["A"]; - - // act - var response = await client.Get(idDiscriminator, null, props, 0, int.MaxValue, new CancellationToken()); - - // assert - Assert.NotNull(response); - Assert.Equal(count, response.Count()); - foreach (var item in response) - { - Assert.Single(item.Set); - var kv = item.Set.First(); - Assert.Equal("A", kv.Key); - } - } - - [Fact] - public async Task Get_geDate() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - var dateMin = DateTimeOffset.Now; - var dateMax = DateTimeOffset.Now.AddSeconds(count); - IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); - var insertResponse = await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - var tail = testSets.OrderBy(t => t.Timestamp).Skip(count / 2).Take(int.MaxValue); - var geDate = tail.First().Timestamp; - var tolerance = TimeSpan.FromSeconds(1); - var expectedCount = tail.Count(); - - // act - var response = await client.Get(idDiscriminator, geDate, null, 0, int.MaxValue, CancellationToken.None); - - // assert - Assert.NotNull(response); - Assert.Equal(expectedCount, response.Count()); - var minDate = response.Min(t => t.Timestamp); - Assert.Equal(geDate, geDate, tolerance); - } - - [Fact] - public async Task Get_with_skip_take() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - var expectedCount = count / 2; - - // act - var response = await client.Get(idDiscriminator, null, null, 2, expectedCount, new CancellationToken()); - - // assert - Assert.NotNull(response); - Assert.Equal(expectedCount, response.Count()); - } - - - [Fact] - public async Task Get_with_big_skip_take() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - var expectedCount = 1; - int count = 10 + expectedCount; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - - // act - var response = await client.Get(idDiscriminator, null, null, count - expectedCount, count, new CancellationToken()); - - // assert - Assert.NotNull(response); - Assert.Equal(expectedCount, response.Count()); - } - - [Fact] - public async Task GetLast() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - var expectedCount = 8; - - // act - var response = await client.GetLast(idDiscriminator, null, expectedCount, new CancellationToken()); - - // assert - Assert.NotNull(response); - Assert.Equal(expectedCount, response.Count()); - } - - [Fact] - public async Task GetDatesRange() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 10; - var dateMin = DateTimeOffset.Now; - var dateMax = DateTimeOffset.Now.AddSeconds(count - 1); - IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - var tolerance = TimeSpan.FromSeconds(1); - - // act - var response = await client.GetDatesRange(idDiscriminator, new CancellationToken()); - - // assert - Assert.NotNull(response); - Assert.Equal(dateMin, response.From, tolerance); - Assert.Equal(dateMax, response.To, tolerance); - } - - [Fact] - public async Task Count() - { - // arrange - Guid idDiscriminator = Guid.NewGuid(); - int count = 144; - IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); - await client.AddRange(idDiscriminator, testSets, CancellationToken.None); - - // act - var response = await client.Count(idDiscriminator, new CancellationToken()); - - // assert - Assert.Equal(count, response); - } - - private static IEnumerable Generate(int n, DateTimeOffset from) - { - for (int i = 0; i < n; i++) - yield return new TimestampedSetDto - ( - from.AddSeconds(i), - new Dictionary{ - {"A", i }, - {"B", i * 1.1 }, - {"C", $"Any{i}" }, - {"D", DateTimeOffset.Now}, - } - ); - } -} diff --git a/DD.Persistence.IntegrationTests/Controllers/TimestampedValuesControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TimestampedValuesControllerTest.cs new file mode 100644 index 0000000..71a49d5 --- /dev/null +++ b/DD.Persistence.IntegrationTests/Controllers/TimestampedValuesControllerTest.cs @@ -0,0 +1,405 @@ +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.Database.Entity; +using DD.Persistence.Models; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using Xunit; + +namespace DD.Persistence.IntegrationTests.Controllers; +public class TimestampedValuesControllerTest : BaseIntegrationTest +{ + private readonly ITimestampedValuesClient timestampedValuesClient; + private readonly IMemoryCache memoryCache; + private IEnumerable discriminatorIds = []; + + public TimestampedValuesControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); + + timestampedValuesClient = scope.ServiceProvider + .GetRequiredService(); + memoryCache = scope.ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task AddRange_returns_success() + { + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + await AddRange(discriminatorId); + } + + [Fact] + public async Task Get_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + //act + var response = await timestampedValuesClient.Get(discriminatorId, null, null, 0, 1, CancellationToken.None); + + //assert + Assert.Null(response); + } + + [Fact] + + public async Task Get_AfterSave_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1); + var columnNames = new List() { "A", "C" }; + var skip = 5; + var take = 5; + + var dtos = await AddRange(discriminatorId); + + //act + var response = await timestampedValuesClient.Get(discriminatorId, timestampBegin, columnNames, skip, take, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + + var actualCount = response.Count(); + Assert.Equal(take, actualCount); + + var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList(); + Assert.Equal(columnNames, actualColumnNames); + + var expectedValueKind = JsonValueKind.Number; + var actualValueKind = ((JsonElement) response.First().Values["A"]).ValueKind; + Assert.Equal(expectedValueKind, actualValueKind); + + expectedValueKind = JsonValueKind.String; + actualValueKind = ((JsonElement)response.First().Values["C"]).ValueKind; + Assert.Equal(expectedValueKind, actualValueKind); + } + + [Fact] + public async Task GetGtDate_returns_success() + { + //arrange + Cleanup(); + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1); + + //act + var response = await timestampedValuesClient.GetGtDate(discriminatorId, timestampBegin, CancellationToken.None); + + //assert + Assert.Null(response); + } + + [Fact] + public async Task GetGtDate_AfterSave_returns_success() + { + //arrange + Cleanup(); + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var dtos = await AddRange(discriminatorId); + var timestampBegin = DateTimeOffset.UtcNow.AddSeconds(-5); + + //act + var response = await timestampedValuesClient.GetGtDate(discriminatorId, timestampBegin, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + + var expectedCount = dtos.Count(dto => dto.Timestamp.ToUniversalTime() > timestampBegin); + var actualCount = response.Count(); + Assert.Equal(expectedCount, actualCount); + } + + [Fact] + public async Task GetFirst_returns_success() + { + //arrange + Cleanup(); + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var take = 1; + + //act + var response = await timestampedValuesClient.GetFirst(discriminatorId, take, CancellationToken.None); + + //assert + Assert.Null(response); + } + + [Fact] + public async Task GetFirst_AfterSave_returns_success() + { + //arrange + Cleanup(); + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var dtos = await AddRange(discriminatorId); + var take = 1; + + //act + var response = await timestampedValuesClient.GetFirst(discriminatorId, take, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + + var expectedTimestampString = dtos + .OrderBy(dto => dto.Timestamp) + .First().Timestamp + .ToUniversalTime() + .ToString(); + var actualTimestampString = response + .First().Timestamp + .ToUniversalTime() + .ToString(); + Assert.Equal(expectedTimestampString, actualTimestampString); + } + + [Fact] + public async Task GetLast_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var take = 1; + + //act + var response = await timestampedValuesClient.GetLast(discriminatorId, take, CancellationToken.None); + + //assert + Assert.Null(response); + } + + [Fact] + public async Task GetLast_AfterSave_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var dtos = await AddRange(discriminatorId); + var take = 1; + + //act + var response = await timestampedValuesClient.GetLast(discriminatorId, take, CancellationToken.None); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + + var expectedTimestampString = dtos + .OrderByDescending(dto => dto.Timestamp) + .First().Timestamp + .ToUniversalTime() + .ToString(); + var actualTimestampString = response + .First().Timestamp + .ToUniversalTime() + .ToString(); + Assert.Equal(expectedTimestampString, actualTimestampString); + } + + [Fact] + public async Task GetResampledData_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var timestampBegin = DateTimeOffset.UtcNow; + + //act + var response = await timestampedValuesClient.GetResampledData(discriminatorId, timestampBegin); + + //assert + Assert.Null(response); + } + + [Fact] + public async Task GetResampledData_AfterSave_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var count = 2048; + var timestampBegin = DateTimeOffset.UtcNow; + var dtos = await AddRange(discriminatorId, count); + + + //act + var response = await timestampedValuesClient.GetResampledData(discriminatorId, timestampBegin, count); + + //assert + Assert.NotNull(response); + Assert.NotEmpty(response); + + var expectedCount = count / 2; + var actualCount = response.Count(); + Assert.Equal(expectedCount, actualCount); + } + + [Fact] + public async Task Count_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + //act + var response = await timestampedValuesClient.Count(discriminatorId, CancellationToken.None); + + //assert + Assert.Equal(0, response); + } + + [Fact] + public async Task Count_AfterSave_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var dtos = await AddRange(discriminatorId); + + //act + var response = await timestampedValuesClient.Count(discriminatorId, CancellationToken.None); + + //assert + var expectedCount = dtos.Count(); + Assert.Equal(expectedCount, response); + } + + [Fact] + public async Task GetDatesRange_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + //act + var response = await timestampedValuesClient.GetDatesRange(discriminatorId, CancellationToken.None); + + //assert + Assert.Null(response); + } + + [Fact] + public async Task GetDatesRange_AfterSave_returns_success() + { + //arrange + Cleanup(); + + var discriminatorId = Guid.NewGuid(); + discriminatorIds.Append(discriminatorId); + + var dtos = await AddRange(discriminatorId); + + //act + var response = await timestampedValuesClient.GetDatesRange(discriminatorId, CancellationToken.None); + + //assert + Assert.NotNull(response); + + var expectedDateFromString = dtos + .OrderBy(dto => dto.Timestamp) + .First().Timestamp + .ToUniversalTime() + .ToString(); + var actualDateFromString = response.From + .ToUniversalTime() + .ToString(); + Assert.Equal(expectedDateFromString, actualDateFromString); + + var expectedDateToString = dtos + .OrderByDescending(dto => dto.Timestamp) + .First().Timestamp + .ToUniversalTime() + .ToString(); + var actualDateToString = response.To + .ToUniversalTime() + .ToString(); + Assert.Equal(expectedDateToString, actualDateToString); + } + + private async Task> AddRange(Guid discriminatorId, int countToCreate = 10) + { + // arrange + IEnumerable generatedDtos = Generate(countToCreate, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + + // act + var response = await timestampedValuesClient.AddRange(discriminatorId, generatedDtos, CancellationToken.None); + + // assert + Assert.Equal(generatedDtos.Count(), response); + + return generatedDtos; + } + + private static IEnumerable Generate(int countToCreate, DateTimeOffset from) + { + var result = new List(); + for (int i = 0; i < countToCreate; i++) + { + var values = new Dictionary() + { + { "A", i }, + { "B", i * 1.1 }, + { "C", $"Any{i}" }, + { "D", DateTimeOffset.Now }, + }; + + yield return new TimestampedValuesDto() + { + Timestamp = from.AddSeconds(i), + Values = values + }; + } + } + + private void Cleanup() + { + discriminatorIds = []; + dbContext.CleanupDbSet(); + dbContext.CleanupDbSet(); + } +} diff --git a/DD.Persistence.IntegrationTests/Controllers/WitsDataControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/WitsDataControllerTest.cs index 6db5853..13e1116 100644 --- a/DD.Persistence.IntegrationTests/Controllers/WitsDataControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/WitsDataControllerTest.cs @@ -1,10 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; +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.Database.Entity; using DD.Persistence.Models; -using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Xunit; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Client; namespace DD.Persistence.IntegrationTests.Controllers; public class WitsDataControllerTest : BaseIntegrationTest @@ -13,11 +15,12 @@ public class WitsDataControllerTest : BaseIntegrationTest public WitsDataControllerTest(WebAppFactoryFixture factory) : base(factory) { - var scope = factory.Services.CreateScope(); - var persistenceClientFactory = scope.ServiceProvider - .GetRequiredService(); + var refitClientFactory = scope.ServiceProvider + .GetRequiredService>(); + var logger = scope.ServiceProvider.GetRequiredService>(); - witsDataClient = persistenceClientFactory.GetWitsDataClient(); + witsDataClient = scope.ServiceProvider + .GetRequiredService(); } [Fact] diff --git a/DD.Persistence.IntegrationTests/TestHttpClientFactory.cs b/DD.Persistence.IntegrationTests/TestHttpClientFactory.cs index b0f86c8..51b8783 100644 --- a/DD.Persistence.IntegrationTests/TestHttpClientFactory.cs +++ b/DD.Persistence.IntegrationTests/TestHttpClientFactory.cs @@ -1,4 +1,7 @@ -namespace DD.Persistence.IntegrationTests +using DD.Persistence.Client.Helpers; +using Microsoft.Extensions.Configuration; + +namespace DD.Persistence.IntegrationTests { /// /// Фабрика HTTP клиентов для интеграционных тестов @@ -6,14 +9,19 @@ public class TestHttpClientFactory : IHttpClientFactory { private readonly WebAppFactoryFixture factory; + private readonly IConfiguration configuration; - public TestHttpClientFactory(WebAppFactoryFixture factory) + public TestHttpClientFactory(WebAppFactoryFixture factory, IConfiguration configuration) { this.factory = factory; + this.configuration = configuration; } public HttpClient CreateClient(string name) { - return factory.CreateClient(); + var client = factory.CreateClient(); + client.Authorize(configuration); + + return client; } } } diff --git a/DD.Persistence.IntegrationTests/WebAppFactoryFixture.cs b/DD.Persistence.IntegrationTests/WebAppFactoryFixture.cs index 76cf71b..0d680b3 100644 --- a/DD.Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/DD.Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -11,6 +11,9 @@ using DD.Persistence.Database.Model; using DD.Persistence.Database.Postgres; using RestSharp; using DD.Persistence.App; +using DD.Persistence.Client.Helpers; +using DD.Persistence.Factories; +using System.Net; namespace DD.Persistence.IntegrationTests; public class WebAppFactoryFixture : WebApplicationFactory @@ -40,12 +43,12 @@ public class WebAppFactoryFixture : WebApplicationFactory services.AddLogging(builder => builder.AddConsole()); services.RemoveAll(); - services.AddSingleton(provider => - { - return new TestHttpClientFactory(this); - }); - - services.AddSingleton(); + services.AddSingleton((provider) => + { + return new TestHttpClientFactory(this, provider.GetRequiredService()); + }); + services.AddHttpClient(); + services.AddPersistenceClients(); var serviceProvider = services.BuildServiceProvider(); diff --git a/DD.Persistence/Models/ChangeLogDto.cs b/DD.Persistence.Models/ChangeLogDto.cs similarity index 100% rename from DD.Persistence/Models/ChangeLogDto.cs rename to DD.Persistence.Models/ChangeLogDto.cs diff --git a/DD.Persistence/Models/Configurations/AuthUser.cs b/DD.Persistence.Models/Configurations/AuthUser.cs similarity index 100% rename from DD.Persistence/Models/Configurations/AuthUser.cs rename to DD.Persistence.Models/Configurations/AuthUser.cs diff --git a/DD.Persistence/Models/Configurations/JwtParams.cs b/DD.Persistence.Models/Configurations/JwtParams.cs similarity index 100% rename from DD.Persistence/Models/Configurations/JwtParams.cs rename to DD.Persistence.Models/Configurations/JwtParams.cs diff --git a/DD.Persistence/Models/Configurations/JwtToken.cs b/DD.Persistence.Models/Configurations/JwtToken.cs similarity index 100% rename from DD.Persistence/Models/Configurations/JwtToken.cs rename to DD.Persistence.Models/Configurations/JwtToken.cs diff --git a/DD.Persistence/Models/Configurations/WitsInfo.cs b/DD.Persistence.Models/Configurations/WitsInfo.cs similarity index 100% rename from DD.Persistence/Models/Configurations/WitsInfo.cs rename to DD.Persistence.Models/Configurations/WitsInfo.cs diff --git a/DD.Persistence.Models/DD.Persistence.Models.csproj b/DD.Persistence.Models/DD.Persistence.Models.csproj new file mode 100644 index 0000000..6facfdc --- /dev/null +++ b/DD.Persistence.Models/DD.Persistence.Models.csproj @@ -0,0 +1,43 @@ + + + + net9.0 + enable + enable + true + + + True + + DD.Persistence.Models + + 1.3.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + + 1.3.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + + DD.Persistence.Models + + + Digital Drilling + + Digital Drilling + + Пакет для получения dtos для работы с Persistence сервисом + + + https://git.ddrilling.ru/on.nemtina/persistence.git + + git + + true + + snupkg + + C:\Projects\Nuget\Persistence + + + + + + + diff --git a/DD.Persistence.Models/DataSchemeDto.cs b/DD.Persistence.Models/DataSchemeDto.cs new file mode 100644 index 0000000..d1efdd5 --- /dev/null +++ b/DD.Persistence.Models/DataSchemeDto.cs @@ -0,0 +1,17 @@ +namespace DD.Persistence.Models; + +/// +/// Схема для набора данных +/// +public class DataSchemeDto +{ + /// + /// Дискриминатор + /// + public Guid DiscriminatorId { get; set; } + + /// + /// Наименования полей + /// + public string[] PropNames { get; set; } = []; +} diff --git a/DD.Persistence/Models/DataSourceSystemDto.cs b/DD.Persistence.Models/DataSourceSystemDto.cs similarity index 87% rename from DD.Persistence/Models/DataSourceSystemDto.cs rename to DD.Persistence.Models/DataSourceSystemDto.cs index 4abe706..2726e91 100644 --- a/DD.Persistence/Models/DataSourceSystemDto.cs +++ b/DD.Persistence.Models/DataSourceSystemDto.cs @@ -13,7 +13,7 @@ public class DataSourceSystemDto /// /// Наименование /// - public required string Name { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; /// /// Описание diff --git a/DD.Persistence/Models/DataWithWellDepthAndSectionDto.cs b/DD.Persistence.Models/DataWithWellDepthAndSectionDto.cs similarity index 100% rename from DD.Persistence/Models/DataWithWellDepthAndSectionDto.cs rename to DD.Persistence.Models/DataWithWellDepthAndSectionDto.cs diff --git a/DD.Persistence/Models/DatesRangeDto.cs b/DD.Persistence.Models/DatesRangeDto.cs similarity index 89% rename from DD.Persistence/Models/DatesRangeDto.cs rename to DD.Persistence.Models/DatesRangeDto.cs index 0a764ce..2049ed7 100644 --- a/DD.Persistence/Models/DatesRangeDto.cs +++ b/DD.Persistence.Models/DatesRangeDto.cs @@ -1,4 +1,4 @@ -namespace DD.Persistence.Models; +namespace DD.Persistence.Models.Common; /// /// Диапазон дат diff --git a/DD.Persistence/Models/Enumerations/EventState.cs b/DD.Persistence.Models/Enumerations/EventState.cs similarity index 100% rename from DD.Persistence/Models/Enumerations/EventState.cs rename to DD.Persistence.Models/Enumerations/EventState.cs diff --git a/DD.Persistence/Models/Enumerations/WitsType.cs b/DD.Persistence.Models/Enumerations/WitsType.cs similarity index 100% rename from DD.Persistence/Models/Enumerations/WitsType.cs rename to DD.Persistence.Models/Enumerations/WitsType.cs diff --git a/DD.Persistence/Models/ITimeSeriesAbstractDto.cs b/DD.Persistence.Models/ITimeSeriesAbstractDto.cs similarity index 59% rename from DD.Persistence/Models/ITimeSeriesAbstractDto.cs rename to DD.Persistence.Models/ITimeSeriesAbstractDto.cs index b01c104..758e7c5 100644 --- a/DD.Persistence/Models/ITimeSeriesAbstractDto.cs +++ b/DD.Persistence.Models/ITimeSeriesAbstractDto.cs @@ -1,12 +1,12 @@ -namespace DD.Persistence.Models; +namespace DD.Persistence.ModelsAbstractions; /// /// Интерфейс, описывающий временные данные /// -public interface ITimeSeriesAbstractDto +public interface ITimestampAbstractDto { /// /// временная отметка /// - DateTimeOffset Date { get; set; } + DateTimeOffset Timestamp { get; set; } } diff --git a/DD.Persistence/Models/IWithSectionPart.cs b/DD.Persistence.Models/IWithSectionPart.cs similarity index 77% rename from DD.Persistence/Models/IWithSectionPart.cs rename to DD.Persistence.Models/IWithSectionPart.cs index 8b3fbaf..0d3ff1f 100644 --- a/DD.Persistence/Models/IWithSectionPart.cs +++ b/DD.Persistence.Models/IWithSectionPart.cs @@ -1,4 +1,4 @@ -namespace DD.Persistence.Models; +namespace DD.Persistence.ModelsAbstractions; public interface IWithSectionPart { public double DepthStart { get; set; } diff --git a/DD.Persistence/Models/MessagesStatisticDto.cs b/DD.Persistence.Models/MessagesStatisticDto.cs similarity index 100% rename from DD.Persistence/Models/MessagesStatisticDto.cs rename to DD.Persistence.Models/MessagesStatisticDto.cs diff --git a/DD.Persistence/Models/PaginationContainer.cs b/DD.Persistence.Models/PaginationContainer.cs similarity index 95% rename from DD.Persistence/Models/PaginationContainer.cs rename to DD.Persistence.Models/PaginationContainer.cs index 89306f8..ec3aa67 100644 --- a/DD.Persistence/Models/PaginationContainer.cs +++ b/DD.Persistence.Models/PaginationContainer.cs @@ -1,4 +1,4 @@ -namespace DD.Persistence.Models; +namespace DD.Persistence.Models.Common; /// /// Контейнер для поддержки постраничного просмотра таблиц diff --git a/DD.Persistence/Models/ParameterDto.cs b/DD.Persistence.Models/ParameterDto.cs similarity index 100% rename from DD.Persistence/Models/ParameterDto.cs rename to DD.Persistence.Models/ParameterDto.cs diff --git a/DD.Persistence/Models/Requests/PaginationRequest.cs b/DD.Persistence.Models/Requests/PaginationRequest.cs similarity index 100% rename from DD.Persistence/Models/Requests/PaginationRequest.cs rename to DD.Persistence.Models/Requests/PaginationRequest.cs diff --git a/DD.Persistence/Models/Requests/SectionPartRequest.cs b/DD.Persistence.Models/Requests/SectionPartRequest.cs similarity index 100% rename from DD.Persistence/Models/Requests/SectionPartRequest.cs rename to DD.Persistence.Models/Requests/SectionPartRequest.cs diff --git a/DD.Persistence/Models/SetpointLogDto.cs b/DD.Persistence.Models/SetpointLogDto.cs similarity index 88% rename from DD.Persistence/Models/SetpointLogDto.cs rename to DD.Persistence.Models/SetpointLogDto.cs index 9a46a99..78cc66c 100644 --- a/DD.Persistence/Models/SetpointLogDto.cs +++ b/DD.Persistence.Models/SetpointLogDto.cs @@ -8,7 +8,7 @@ public class SetpointLogDto : SetpointValueDto /// /// Дата сохранения уставки /// - public DateTimeOffset Created { get; set; } + public DateTimeOffset Timestamp { get; set; } /// /// Ключ пользователя diff --git a/DD.Persistence/Models/SetpointValueDto.cs b/DD.Persistence.Models/SetpointValueDto.cs similarity index 100% rename from DD.Persistence/Models/SetpointValueDto.cs rename to DD.Persistence.Models/SetpointValueDto.cs diff --git a/DD.Persistence/Models/TechMessageDto.cs b/DD.Persistence.Models/TechMessageDto.cs similarity index 100% rename from DD.Persistence/Models/TechMessageDto.cs rename to DD.Persistence.Models/TechMessageDto.cs diff --git a/DD.Persistence.Models/TimestampedValuesDto.cs b/DD.Persistence.Models/TimestampedValuesDto.cs new file mode 100644 index 0000000..13b592e --- /dev/null +++ b/DD.Persistence.Models/TimestampedValuesDto.cs @@ -0,0 +1,19 @@ +using DD.Persistence.ModelsAbstractions; + +namespace DD.Persistence.Models; + +/// +/// Набор данных с отметкой времени +/// +public class TimestampedValuesDto : ITimestampAbstractDto +{ + /// + /// Временная отметка + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Набор данных + /// + public Dictionary Values { get; set; } = []; +} diff --git a/DD.Persistence/Models/WitsDataDto.cs b/DD.Persistence.Models/WitsDataDto.cs similarity index 100% rename from DD.Persistence/Models/WitsDataDto.cs rename to DD.Persistence.Models/WitsDataDto.cs diff --git a/DD.Persistence/Models/WitsValueDto.cs b/DD.Persistence.Models/WitsValueDto.cs similarity index 100% rename from DD.Persistence/Models/WitsValueDto.cs rename to DD.Persistence.Models/WitsValueDto.cs diff --git a/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj b/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj new file mode 100644 index 0000000..4f29d86 --- /dev/null +++ b/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DD.Persistence.Repository.Test/RepositoryTestFixture.cs b/DD.Persistence.Repository.Test/RepositoryTestFixture.cs new file mode 100644 index 0000000..81e3694 --- /dev/null +++ b/DD.Persistence.Repository.Test/RepositoryTestFixture.cs @@ -0,0 +1,32 @@ + +using DD.Persistence.Database; +using DD.Persistence.Database.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Testcontainers.PostgreSql; +using Xunit; + +namespace DD.Persistence.Repository.Test; + +public class RepositoryTestFixture : IAsyncLifetime +{ + public readonly PostgreSqlContainer dbContainer = new PostgreSqlBuilder().Build(); + + + public PersistencePostgresContext GetDbContext() => new(new DbContextOptionsBuilder() + .UseNpgsql(dbContainer.GetConnectionString()).Options); + + public IMemoryCache GetMemoryCache() => new MemoryCache(new MemoryCacheOptions()); + + public virtual async Task InitializeAsync() + { + await dbContainer.StartAsync(); + var forumDbContext = new PersistencePostgresContext(new DbContextOptionsBuilder() + .UseNpgsql(dbContainer.GetConnectionString()).Options); + + await forumDbContext.Database.MigrateAsync(); + } + + public async Task DisposeAsync() => await dbContainer.DisposeAsync(); +} + diff --git a/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs b/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs new file mode 100644 index 0000000..6b05ff3 --- /dev/null +++ b/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs @@ -0,0 +1,56 @@ +using DD.Persistence.Database.Model; +using DD.Persistence.Repository.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 +{ + private readonly RepositoryTestFixture fixture; + private readonly PersistencePostgresContext context; + private readonly SetpointRepository sut; + + public SetpointRepositoryShould(RepositoryTestFixture fixture) + { + this.fixture = fixture; + context = fixture.GetDbContext(); + sut = new SetpointRepository(context); + } + + [Fact] + public async Task ReturnValueKindNumber() + { + var id = Guid.NewGuid(); + 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); + + + //assert + result.ShouldNotBeNull(); + result.ShouldNotBeEmpty(); + + var setpoint = result.First(); + + setpoint.Value.ShouldNotBeNull(); + setpoint + .Value.ShouldBeOfType() + .ValueKind.ShouldBe(JsonValueKind.Number); + } + + private JsonElement GetJsonFromObject(object value) + { + var jsonString = JsonSerializer.Serialize(value); + var doc = JsonDocument.Parse(jsonString); + return doc.RootElement; + } + +} diff --git a/DD.Persistence.Repository/DependencyInjection.cs b/DD.Persistence.Repository/DependencyInjection.cs index df15095..d0a5d37 100644 --- a/DD.Persistence.Repository/DependencyInjection.cs +++ b/DD.Persistence.Repository/DependencyInjection.cs @@ -6,6 +6,7 @@ using DD.Persistence.Repositories; using DD.Persistence.Repository.Repositories; using DD.Persistence.Database.Entity; using System.Reflection; +using DD.Persistence.Repository.RepositoriesCached; namespace DD.Persistence.Repository; public static class DependencyInjection @@ -35,15 +36,14 @@ public static class DependencyInjection MapsterSetup(); - services.AddTransient, TimeSeriesDataRepository>(); services.AddTransient(); - services.AddTransient, TimeSeriesDataCachedRepository>(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); - return services; + return services; } } diff --git a/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs b/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs index bdfc8a2..bed529e 100644 --- a/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs +++ b/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs @@ -6,7 +6,7 @@ namespace DD.Persistence.Repository.Extensions; public static class EFExtensionsSortBy { - struct TypeAccessor + public struct TypeAccessor { public LambdaExpression KeySelector { get; set; } public MethodInfo OrderBy { get; set; } @@ -26,6 +26,42 @@ public static class EFExtensionsSortBy private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending"); + public static Func> sortOrder = + (Type rootType, Type? type, TypeAccessor? accessor) => + { + if (type is null && accessor.HasValue) + { + var accessorValue = accessor.Value; + return Tuple.Create( + accessorValue.OrderBy, + accessorValue.OrderByDescending + ); + } + + return Tuple.Create( + methodOrderBy.MakeGenericMethod(rootType, type!), + methodOrderByDescending.MakeGenericMethod(rootType, type!) + ); + }; + + public static Func> thenSortOrder = + (Type rootType, Type? type, TypeAccessor? accessor) => + { + if (type is null && accessor.HasValue) + { + var accessorValue = accessor.Value; + return Tuple.Create( + accessorValue.ThenBy, + accessorValue.ThenByDescending + ); + } + + return Tuple.Create( + methodThenBy.MakeGenericMethod(rootType, type!), + methodThenByDescending.MakeGenericMethod(rootType, type!) + ); + }; + private static MethodInfo GetExtOrderMethod(string methodName) => typeof(Queryable) .GetMethods() @@ -83,10 +119,11 @@ public static class EFExtensionsSortBy var sortEnum = propertySorts.GetEnumerator(); sortEnum.MoveNext(); - var orderedQuery = query.SortBy(sortEnum.Current); + + var orderedQuery = query.SortBy(sortOrder, sortEnum.Current); while (sortEnum.MoveNext()) - orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current); + orderedQuery = orderedQuery.SortBy(thenSortOrder, sortEnum.Current); return orderedQuery; } @@ -108,39 +145,14 @@ public static class EFExtensionsSortBy /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( this IQueryable query, + Func> orderMethod, string propertySort) { var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; var propertyName = parts[0]; - var newQuery = query.SortBy(propertyName, isDesc); - return newQuery; - } - - /// - /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию. - /// - /// - /// - /// - /// Свойство сортировки. - /// Состоит из названия свойства (в любом регистре) - /// и опционально указания направления сортировки "asc" или "desc" - /// - /// - /// var query = query("Date desc"); - /// - /// Запрос с примененной сортировкой - public static IOrderedQueryable ThenSortBy( - this IOrderedQueryable query, - string propertySort) - { - var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); - var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; - var propertyName = parts[0]; - - var newQuery = query.ThenSortBy(propertyName, isDesc); + var newQuery = query.SortBy(orderMethod, propertyName, isDesc); return newQuery; } @@ -154,25 +166,27 @@ public static class EFExtensionsSortBy /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( this IQueryable query, + Func> orderMethod, string propertyName, bool isDesc) { Type rootType = typeof(TSource); - var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); - var propertyNameLower = propertyName.ToLower(); MethodInfo orderByDescending; MethodInfo orderByAscending; + TypeAccessor? rootTypeAccessor = null; + Type? type = null; LambdaExpression? lambdaExpression = null; - if (propertyName.Contains('.')) + const string Separator = "."; + if (propertyName.Contains(Separator)) { - Type type = rootType; + type = rootType; ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); Expression expr = rootExpression; - var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); + var propertyPath = propertyName.Split(Separator, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < propertyPath.Length; i++) { @@ -184,75 +198,24 @@ public static class EFExtensionsSortBy Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); - orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type); - orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type); + Tuple order = orderMethod + .Invoke(rootType, type, null); + orderByAscending = order.Item1; + orderByDescending = order.Item2; } else { - var rootTypeAccessor = typePropSelector[propertyNameLower]; - orderByAscending = rootTypeAccessor.OrderBy; - orderByDescending = rootTypeAccessor.OrderByDescending; - lambdaExpression = rootTypeAccessor.KeySelector; - } + var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); + var propertyNameLower = propertyName.ToLower(); - var genericMethod = isDesc - ? orderByDescending - : orderByAscending; + rootTypeAccessor = typePropSelector[propertyNameLower]; - var newQuery = (IOrderedQueryable)genericMethod - .Invoke(genericMethod, [query, lambdaExpression])!; - return newQuery; - } + Tuple order = orderMethod + .Invoke(rootType, type, rootTypeAccessor); + orderByAscending = order.Item1; + orderByDescending = order.Item2; - /// - /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию - /// - /// - /// - /// Название свойства (в любом регистре) - /// Сортировать по убыванию - /// Запрос с примененной сортировкой - public static IOrderedQueryable ThenSortBy( - this IOrderedQueryable query, - string propertyName, - bool isDesc) - { - Type rootType = typeof(TSource); - var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); - var propertyNameLower = propertyName.ToLower(); - - MethodInfo orderByDescending; - MethodInfo orderByAscending; - - LambdaExpression? lambdaExpression = null; - - if (propertyName.Contains('.')) - { - Type type = rootType; - ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); - Expression expr = rootExpression; - - var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); - - for (int i = 0; i < propertyPath.Length; i++) - { - PropertyInfo pi = type.GetProperty(propertyPath[i])!; - expr = Expression.Property(expr, pi); - type = pi.PropertyType; - } - - Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); - lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); - - orderByAscending = methodThenBy.MakeGenericMethod(rootType, type); - orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type); - } - else - { - var rootTypeAccessor = typePropSelector[propertyNameLower]; - orderByAscending = rootTypeAccessor.ThenBy; - orderByDescending = rootTypeAccessor.ThenByDescending; - lambdaExpression = rootTypeAccessor.KeySelector; + lambdaExpression = rootTypeAccessor.Value.KeySelector; } var genericMethod = isDesc diff --git a/DD.Persistence.Repository/QueryBuilders.cs b/DD.Persistence.Repository/QueryBuilders.cs index 6fad2cc..53e72a4 100644 --- a/DD.Persistence.Repository/QueryBuilders.cs +++ b/DD.Persistence.Repository/QueryBuilders.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; -using DD.Persistence.Database.Model; -using DD.Persistence.Models; using DD.Persistence.Models.Requests; +using DD.Persistence.Models.Common; +using DD.Persistence.ModelsAbstractions; +using DD.Persistence.Database.EntityAbstractions; +using DD.Persistence.Extensions; namespace DD.Persistence.Repository; diff --git a/DD.Persistence.Repository/Repositories/ChangeLogRepository.cs b/DD.Persistence.Repository/Repositories/ChangeLogRepository.cs index ef57d10..60fe08e 100644 --- a/DD.Persistence.Repository/Repositories/ChangeLogRepository.cs +++ b/DD.Persistence.Repository/Repositories/ChangeLogRepository.cs @@ -5,6 +5,7 @@ using DD.Persistence.Models; using DD.Persistence.Models.Requests; using DD.Persistence.Repositories; using UuidExtensions; +using DD.Persistence.Models.Common; namespace DD.Persistence.Repository.Repositories; public class ChangeLogRepository : IChangeLogRepository diff --git a/DD.Persistence.Repository/Repositories/DataSchemeRepository.cs b/DD.Persistence.Repository/Repositories/DataSchemeRepository.cs new file mode 100644 index 0000000..c30c8da --- /dev/null +++ b/DD.Persistence.Repository/Repositories/DataSchemeRepository.cs @@ -0,0 +1,34 @@ +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 GetQueryReadOnly() => db.Set(); + + public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token) + { + var entity = dataSourceSystemDto.Adapt(); + + await db.Set().AddAsync(entity, token); + await db.SaveChangesAsync(token); + } + + public virtual async Task 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()).FirstOrDefault(); + + return dto; + } +} diff --git a/DD.Persistence.Repository/Repositories/ParameterRepository.cs b/DD.Persistence.Repository/Repositories/ParameterRepository.cs index 434d59f..d241de7 100644 --- a/DD.Persistence.Repository/Repositories/ParameterRepository.cs +++ b/DD.Persistence.Repository/Repositories/ParameterRepository.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using DD.Persistence.Database.Entity; using DD.Persistence.Models; using DD.Persistence.Repositories; +using DD.Persistence.Models.Common; namespace DD.Persistence.Repository.Repositories; public class ParameterRepository : IParameterRepository diff --git a/DD.Persistence.Repository/Repositories/SetpointRepository.cs b/DD.Persistence.Repository/Repositories/SetpointRepository.cs index f7a719a..8c3ae65 100644 --- a/DD.Persistence.Repository/Repositories/SetpointRepository.cs +++ b/DD.Persistence.Repository/Repositories/SetpointRepository.cs @@ -1,8 +1,10 @@ -using Mapster; +using Mapster; using Microsoft.EntityFrameworkCore; using DD.Persistence.Database.Model; using DD.Persistence.Models; using DD.Persistence.Repositories; +using System.Text.Json; +using DD.Persistence.Models.Common; namespace DD.Persistence.Repository.Repositories { @@ -16,16 +18,33 @@ namespace DD.Persistence.Repository.Repositories protected virtual IQueryable GetQueryReadOnly() => db.Set(); - public async Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token) + public async Task> GetCurrent( + IEnumerable setpointKeys, + CancellationToken token) { var query = GetQueryReadOnly(); + var entities = await query .Where(e => setpointKeys.Contains(e.Key)) + .GroupBy(e => e.Key) + .Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault()) .ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); + var dtos = entities.Select(e => e.Adapt()); return dtos; } + public async Task> GetCurrentDictionary(IEnumerable setpointKeys, CancellationToken token) + { + var query = GetQueryReadOnly(); + + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .GroupBy(e => e.Key) + .Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault()) + .ToDictionaryAsync(x=> x.Key, x => (object)x.Value, token); + + return entities; + } public async Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) { @@ -35,8 +54,8 @@ namespace DD.Persistence.Repository.Repositories .ToArrayAsync(token); var filteredEntities = entities .GroupBy(e => e.Key) - .Select(e => e.OrderBy(o => o.Created)) - .Select(e => e.Where(e => e.Created <= historyMoment).Last()); + .Select(e => e.OrderBy(o => o.Timestamp)) + .Select(e => e.Where(e => e.Timestamp <= historyMoment).Last()); var dtos = filteredEntities .Select(e => e.Adapt()); @@ -47,7 +66,7 @@ namespace DD.Persistence.Repository.Repositories { var query = GetQueryReadOnly(); var entities = await query - .Where(e => e.Created >= dateBegin) + .Where(e => e.Timestamp >= dateBegin) .Take(take) .ToArrayAsync(token); var dtos = entities @@ -62,8 +81,8 @@ namespace DD.Persistence.Repository.Repositories .GroupBy(e => 1) .Select(group => new { - Min = group.Min(e => e.Created), - Max = group.Max(e => e.Created), + Min = group.Min(e => e.Timestamp), + Max = group.Max(e => e.Timestamp), }); var values = await query.FirstOrDefaultAsync(token); var result = new DatesRangeDto() @@ -88,18 +107,20 @@ namespace DD.Persistence.Repository.Repositories return dtos; } - public async Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token) + public async Task Add(Guid setpointKey, JsonElement newValue, Guid idUser, CancellationToken token) { var entity = new Setpoint() { Key = setpointKey, Value = newValue, IdUser = idUser, - Created = DateTimeOffset.UtcNow + Timestamp = DateTimeOffset.UtcNow.ToUniversalTime() }; await db.Set().AddAsync(entity, token); await db.SaveChangesAsync(token); } + + } } diff --git a/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs b/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs index 6bbb382..1a84f25 100644 --- a/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs +++ b/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs @@ -1,16 +1,15 @@ -using Mapster; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json.Linq; using DD.Persistence.Database.Entity; +using DD.Persistence.Extensions; using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; using DD.Persistence.Repositories; -using UuidExtensions; +using Mapster; +using Microsoft.EntityFrameworkCore; namespace DD.Persistence.Repository.Repositories { - public class TechMessagesRepository : ITechMessagesRepository + public class TechMessagesRepository : ITechMessagesRepository { private readonly IDataSourceSystemRepository sourceSystemRepository; private DbContext db; @@ -91,6 +90,7 @@ namespace DD.Persistence.Repository.Repositories await CreateSystemIfNotExist(systemId, token); entity.SystemId = systemId; + entity.Timestamp = dto.Timestamp.ToUniversalTime(); entities.Add(entity); } diff --git a/DD.Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs b/DD.Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs deleted file mode 100644 index 768f747..0000000 --- a/DD.Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using DD.Persistence.Database.Model; -using DD.Persistence.Models; - -namespace DD.Persistence.Repository.Repositories; - -public class TimeSeriesDataCachedRepository : TimeSeriesDataRepository - where TEntity : class, ITimestampedData, new() - where TDto : class, ITimeSeriesAbstractDto, new() -{ - public static TDto? FirstByDate { get; private set; } - public static CyclicArray LastData { get; } = new CyclicArray(CacheItemsCount); - - private const int CacheItemsCount = 3600; - - public TimeSeriesDataCachedRepository(DbContext db) : base(db) - { - Task.Run(async () => - { - var firstDateItem = await base.GetFirstAsync(CancellationToken.None); - if (firstDateItem == null) - { - return; - } - - FirstByDate = firstDateItem; - - var dtos = await base.GetLastAsync(CacheItemsCount, CancellationToken.None); - dtos = dtos.OrderBy(d => d.Date); - LastData.AddRange(dtos); - }).Wait(); - } - - public override async Task> GetGtDate(DateTimeOffset dateBegin, CancellationToken token) - { - - if (LastData.Count == 0 || LastData[0].Date > dateBegin) - { - var dtos = await base.GetGtDate(dateBegin, token); - return dtos; - } - - var items = LastData - .Where(i => i.Date >= dateBegin); - - return items; - } - - public override async Task AddRange(IEnumerable dtos, CancellationToken token) - { - var result = await base.AddRange(dtos, token); - if (result > 0) - { - - dtos = dtos.OrderBy(x => x.Date); - - FirstByDate = dtos.First(); - LastData.AddRange(dtos); - } - - return result; - } - - public override async Task GetDatesRange(CancellationToken token) - { - if (FirstByDate == null) - return null; - - return await Task.Run(() => - { - return new DatesRangeDto - { - From = FirstByDate.Date, - To = LastData[^1].Date - }; - }); - } - - public override async Task> GetResampledData( - DateTimeOffset dateBegin, - double intervalSec = 600d, - int approxPointsCount = 1024, - CancellationToken token = default) - { - var dtos = LastData.Where(i => i.Date >= dateBegin); - if (LastData.Count == 0 || LastData[0].Date > dateBegin) - { - dtos = await base.GetGtDate(dateBegin, token); - } - - var dateEnd = dateBegin.AddSeconds(intervalSec); - dtos = dtos - .Where(i => i.Date <= dateEnd); - - var ratio = dtos.Count() / approxPointsCount; - if (ratio > 1) - dtos = dtos - .Where((_, index) => index % ratio == 0); - - return dtos; - } -} - diff --git a/DD.Persistence.Repository/Repositories/TimeSeriesDataRepository.cs b/DD.Persistence.Repository/Repositories/TimeSeriesDataRepository.cs deleted file mode 100644 index c4c634b..0000000 --- a/DD.Persistence.Repository/Repositories/TimeSeriesDataRepository.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Mapster; -using Microsoft.EntityFrameworkCore; -using DD.Persistence.Database.Model; -using DD.Persistence.Models; -using DD.Persistence.Repositories; - -namespace DD.Persistence.Repository.Repositories; -public class TimeSeriesDataRepository : ITimeSeriesDataRepository - where TEntity : class, ITimestampedData, new() - where TDto : class, ITimeSeriesAbstractDto, new() -{ - private readonly DbContext db; - - public TimeSeriesDataRepository(DbContext db) - { - this.db = db; - } - - protected virtual IQueryable GetQueryReadOnly() => this.db.Set(); - - public virtual async Task GetDatesRange(CancellationToken token) - { - var query = GetQueryReadOnly(); - var minDate = await query.MinAsync(o => o.Date, token); - var maxDate = await query.MaxAsync(o => o.Date, token); - - return new DatesRangeDto - { - From = minDate, - To = maxDate - }; - } - - public virtual async Task> GetGtDate(DateTimeOffset date, CancellationToken token) - { - var query = this.db.Set().Where(e => e.Date > date); - var entities = await query.ToArrayAsync(token); - - var dtos = entities.Select(e => e.Adapt()); - - return dtos; - } - - public virtual async Task AddRange(IEnumerable dtos, CancellationToken token) - { - var entities = dtos.Select(d => d.Adapt()); - - await db.Set().AddRangeAsync(entities, token); - var result = await db.SaveChangesAsync(token); - - return result; - } - - protected async Task> GetLastAsync(int takeCount, CancellationToken token) - { - var query = GetQueryReadOnly() - .OrderByDescending(e => e.Date) - .Take(takeCount); - - var entities = await query.ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); - - return dtos; - } - - protected async Task GetFirstAsync(CancellationToken token) - { - var query = GetQueryReadOnly() - .OrderBy(e => e.Date); - - var entity = await query.FirstOrDefaultAsync(token); - - if (entity == null) - return null; - - var dto = entity.Adapt(); - return dto; - } - - public async virtual Task> GetResampledData( - DateTimeOffset dateBegin, - double intervalSec = 600d, - int approxPointsCount = 1024, - CancellationToken token = default) - { - var dtos = await GetGtDate(dateBegin, token); - - var dateEnd = dateBegin.AddSeconds(intervalSec); - dtos = dtos - .Where(i => i.Date <= dateEnd); - - var ratio = dtos.Count() / approxPointsCount; - if (ratio > 1) - dtos = dtos - .Where((_, index) => index % ratio == 0); - - return dtos; - } -} diff --git a/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs b/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs deleted file mode 100644 index 6f54521..0000000 --- a/DD.Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using DD.Persistence.Database.Entity; -using DD.Persistence.Models; -using DD.Persistence.Repositories; - -namespace DD.Persistence.Repository.Repositories; - -/// -/// Репозиторий для хранения разных наборов данных временных рядов. -/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. -/// idDiscriminator формируют клиенты и только им известно что они обозначают. -/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. -/// -public class TimestampedSetRepository : ITimestampedSetRepository -{ - private readonly DbContext db; - - public TimestampedSetRepository(DbContext db) - { - this.db = db; - } - - public Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) - { - var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set)); - var dbSet = db.Set(); - dbSet.AddRange(entities); - return db.SaveChangesAsync(token); - } - - public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) - { - var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); - - if (geTimestamp.HasValue) - query = ApplyGeTimestamp(query, geTimestamp.Value); - - query = query - .OrderBy(item => item.Timestamp) - .Skip(skip) - .Take(take); - - var data = await Materialize(query, token); - - if (columnNames is not null && columnNames.Any()) - data = ReduceSetColumnsByNames(data, columnNames); - - return data; - } - - public async Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token) - { - var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); - - query = query.OrderByDescending(entity => entity.Timestamp) - .Take(take) - .OrderBy(entity => entity.Timestamp); - - var data = await Materialize(query, token); - - if (columnNames is not null && columnNames.Any()) - data = ReduceSetColumnsByNames(data, columnNames); - - return data; - } - - public Task Count(Guid idDiscriminator, CancellationToken token) - { - var dbSet = db.Set(); - var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); - return query.CountAsync(token); - } - - public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) - { - var query = db.Set() - .GroupBy(entity => entity.IdDiscriminator) - .Select(group => new - { - Min = group.Min(entity => entity.Timestamp), - Max = group.Max(entity => entity.Timestamp), - }); - - var item = await query.FirstOrDefaultAsync(token); - if (item is null) - return null; - - return new DatesRangeDto - { - From = item.Min, - To = item.Max, - }; - } - - private static async Task> Materialize(IQueryable query, CancellationToken token) - { - var dtoQuery = query.Select(entity => new TimestampedSetDto(entity.Timestamp, entity.Set)); - var dtos = await dtoQuery.ToArrayAsync(token); - return dtos; - } - - private static IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset geTimestamp) - { - var geTimestampUtc = geTimestamp.ToUniversalTime(); - return query.Where(entity => entity.Timestamp >= geTimestampUtc); - } - - private static IEnumerable ReduceSetColumnsByNames(IEnumerable query, IEnumerable columnNames) - { - var newQuery = query - .Select(entity => new TimestampedSetDto( - entity.Timestamp, - entity.Set - .Where(prop => columnNames.Contains(prop.Key)) - .ToDictionary(prop => prop.Key, prop => prop.Value) - )); - return newQuery; - } -} diff --git a/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs b/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs new file mode 100644 index 0000000..204b1a2 --- /dev/null +++ b/DD.Persistence.Repository/Repositories/TimestampedValuesRepository.cs @@ -0,0 +1,179 @@ +using DD.Persistence.Database.Entity; +using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using DD.Persistence.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace DD.Persistence.Repository.Repositories; +public class TimestampedValuesRepository : ITimestampedValuesRepository +{ + private readonly DbContext db; + + public TimestampedValuesRepository(DbContext db) + { + this.db = db; + } + + protected virtual IQueryable GetQueryReadOnly() => this.db.Set(); + + public async virtual Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token) + { + var timestampedValuesEntities = new List(); + foreach (var dto in dtos) + { + var timestampedValuesEntity = new TimestampedValues() + { + DiscriminatorId = discriminatorId, + Timestamp = dto.Timestamp.ToUniversalTime(), + Values = dto.Values.Values.ToArray() + }; + timestampedValuesEntities.Add(timestampedValuesEntity); + } + + await db.Set().AddRangeAsync(timestampedValuesEntities, token); + + var result = await db.SaveChangesAsync(token); + + return result; + } + + public async virtual Task>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable? columnNames, int skip, int take, CancellationToken token) + { + var query = GetQueryReadOnly() + .Where(entity => entity.DiscriminatorId == discriminatorId); + + // Фильтрация по дате + if (timestampBegin.HasValue) + { + query = ApplyGeTimestamp(query, timestampBegin.Value); + } + + query = query + .OrderBy(item => item.Timestamp) + .Skip(skip) + .Take(take); + var entities = await query.ToArrayAsync(token); + + var result = entities.Select(e => Tuple.Create( + e.Timestamp, + e.Values + )); + + return result; + } + + public async virtual Task>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token) + { + var query = GetQueryReadOnly() + .OrderBy(e => e.Timestamp) + .Take(takeCount); + var entities = await query.ToArrayAsync(token); + + var result = entities.Select(e => Tuple.Create( + e.Timestamp, + e.Values + )); + + return result; + } + + public async virtual Task>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token) + { + var query = GetQueryReadOnly() + .OrderByDescending(e => e.Timestamp) + .Take(takeCount); + var entities = await query.ToArrayAsync(token); + + var result = entities.Select(e => Tuple.Create( + e.Timestamp, + e.Values + )); + + return result; + } + + // ToDo: прореживание должно осуществляться до материализации + public async virtual Task>> GetResampledData( + Guid discriminatorId, + DateTimeOffset dateBegin, + double intervalSec = 600d, + int approxPointsCount = 1024, + CancellationToken token = default) + { + var result = await GetGtDate(discriminatorId, dateBegin, token); + + var dateEnd = dateBegin.AddSeconds(intervalSec); + result = result + .Where(i => i.Item1 <= dateEnd); + + var ratio = result.Count() / approxPointsCount; + if (ratio > 1) + result = result + .Where((_, index) => index % ratio == 0); + + return result; + } + + public async virtual Task>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token) + { + var query = GetQueryReadOnly() + .Where(e => e.Timestamp > timestampBegin); + var entities = await query.ToArrayAsync(token); + + var result = entities.Select(e => Tuple.Create( + e.Timestamp, + e.Values + )); + + return result; + } + + public async virtual Task GetDatesRange(Guid discriminatorId, CancellationToken token) + { + var query = GetQueryReadOnly() + .GroupBy(entity => entity.DiscriminatorId) + .Select(group => new + { + Min = group.Min(entity => entity.Timestamp), + Max = group.Max(entity => entity.Timestamp), + }); + + var item = await query.FirstOrDefaultAsync(token); + if (item is null) + { + return null; + } + + var dto = new DatesRangeDto + { + From = item.Min, + To = item.Max, + }; + + return dto; + } + + public virtual Task Count(Guid discriminatorId, CancellationToken token) + { + var dbSet = db.Set(); + var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId); + + return query.CountAsync(token); + } + + /// + /// Применить фильтр по дате + /// + /// + /// + /// + private IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset timestampBegin) + { + var geTimestampUtc = timestampBegin.ToUniversalTime(); + + var result = query + .Where(entity => entity.Timestamp >= geTimestampUtc); + + return result; + } +} diff --git a/DD.Persistence.Repository/RepositoriesCached/DataSchemeCachedRepository.cs b/DD.Persistence.Repository/RepositoriesCached/DataSchemeCachedRepository.cs new file mode 100644 index 0000000..ea22196 --- /dev/null +++ b/DD.Persistence.Repository/RepositoriesCached/DataSchemeCachedRepository.cs @@ -0,0 +1,30 @@ +using DD.Persistence.Models; +using DD.Persistence.Repository.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace DD.Persistence.Repository.RepositoriesCached; +public class DataSchemeCachedRepository : DataSchemeRepository +{ + private readonly IMemoryCache memoryCache; + + public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) + { + this.memoryCache = memoryCache; + } + + public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token) + { + await base.Add(dataSourceSystemDto, token); + + memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto); + } + + public override async Task Get(Guid discriminatorId, CancellationToken token) + { + var result = memoryCache.Get(discriminatorId) + ?? await base.Get(discriminatorId, token); + + return result; + } +} diff --git a/DD.Persistence.Repository/Repositories/DataSourceSystemCachedRepository.cs b/DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs similarity index 83% rename from DD.Persistence.Repository/Repositories/DataSourceSystemCachedRepository.cs rename to DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs index 37009e8..87e487e 100644 --- a/DD.Persistence.Repository/Repositories/DataSourceSystemCachedRepository.cs +++ b/DD.Persistence.Repository/RepositoriesCached/DataSourceSystemCachedRepository.cs @@ -1,13 +1,14 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; +using DD.Persistence.Database.Entity; using DD.Persistence.Models; +using DD.Persistence.Repository.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; -namespace DD.Persistence.Repository.Repositories; +namespace DD.Persistence.Repository.RepositoriesCached; public class DataSourceSystemCachedRepository : DataSourceSystemRepository { - private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DataSourceSystem).FullName}CacheKey"; + private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey"; private readonly IMemoryCache memoryCache; - private const int CacheExpirationInMinutes = 60; private readonly TimeSpan? AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); public DataSourceSystemCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) diff --git a/DD.Persistence.Repository/RepositoriesCached/TimestampedValuesCachedRepository.cs b/DD.Persistence.Repository/RepositoriesCached/TimestampedValuesCachedRepository.cs new file mode 100644 index 0000000..f81f7a0 --- /dev/null +++ b/DD.Persistence.Repository/RepositoriesCached/TimestampedValuesCachedRepository.cs @@ -0,0 +1,103 @@ +//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 LastData { get; } = new CyclicArray(CacheItemsCount); + +// private const int CacheItemsCount = 3600; + +// public TimestampedValuesCachedRepository(DbContext db, IDataSourceSystemRepository 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> 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 AddRange(Guid discriminatorId, IEnumerable 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 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> 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; +// } +//} + diff --git a/DD.Persistence.Test/DD.Persistence.Test.csproj b/DD.Persistence.Test/DD.Persistence.Test.csproj new file mode 100644 index 0000000..8efaa34 --- /dev/null +++ b/DD.Persistence.Test/DD.Persistence.Test.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/DD.Persistence.Test/TimestampedValuesServiceShould.cs b/DD.Persistence.Test/TimestampedValuesServiceShould.cs new file mode 100644 index 0000000..5f3e65b --- /dev/null +++ b/DD.Persistence.Test/TimestampedValuesServiceShould.cs @@ -0,0 +1,58 @@ +using DD.Persistence.Models; +using DD.Persistence.Repositories; +using DD.Persistence.Services; +using NSubstitute; + +namespace DD.Persistence.Repository.Test; +public class TimestampedValuesServiceShould +{ + private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For(); + private readonly IDataSchemeRepository dataSchemeRepository = Substitute.For(); + private TimestampedValuesService timestampedValuesService; + + public TimestampedValuesServiceShould() + { + timestampedValuesService = new TimestampedValuesService(timestampedValuesRepository, dataSchemeRepository); + } + + [Fact] + public async Task TestServiceEfficiency() + { + var discriminatorId = Guid.NewGuid(); + const int count = 10; + var dtos = Generate(count, DateTimeOffset.UtcNow); + var addRangeResult = await timestampedValuesService + .AddRange(discriminatorId, dtos, CancellationToken.None); + Assert.Equal(0, addRangeResult); + + var columnNames = new[] { "A", "B", "C", "D" }; + var geTimestamp = DateTimeOffset.UtcNow + .AddHours(-1) + .ToUniversalTime(); + var getResult = await timestampedValuesService + .Get(discriminatorId, geTimestamp, columnNames, 0, count, CancellationToken.None); + Assert.NotNull(getResult); + Assert.Empty(getResult); + } + + private static IEnumerable Generate(int countToCreate, DateTimeOffset from) + { + var result = new List(); + for (int i = 0; i < countToCreate; i++) + { + var values = new Dictionary() + { + { "A", i }, + { "B", i * 1.1 }, + { "C", $"Any{i}" }, + { "D", DateTimeOffset.Now }, + }; + + yield return new TimestampedValuesDto() + { + Timestamp = from.AddSeconds(i), + Values = values + }; + } + } +} diff --git a/DD.Persistence.sln b/DD.Persistence.sln index 22c6b2a..03f0be9 100644 --- a/DD.Persistence.sln +++ b/DD.Persistence.sln @@ -19,6 +19,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.Client", "DD EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.App", "DD.Persistence.App\DD.Persistence.App.csproj", "{063238BF-E982-43FA-9DDB-7D7D279086D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Models", "DD.Persistence.Models\DD.Persistence.Models.csproj", "{698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Repository.Test", "DD.Persistence.Repository.Test\DD.Persistence.Repository.Test.csproj", "{08B03623-A1C9-482F-B60E-09F293E04999}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Test", "DD.Persistence.Test\DD.Persistence.Test.csproj", "{B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +68,18 @@ Global {063238BF-E982-43FA-9DDB-7D7D279086D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {063238BF-E982-43FA-9DDB-7D7D279086D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {063238BF-E982-43FA-9DDB-7D7D279086D8}.Release|Any CPU.Build.0 = Release|Any CPU + {698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Release|Any CPU.Build.0 = Release|Any CPU + {08B03623-A1C9-482F-B60E-09F293E04999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08B03623-A1C9-482F-B60E-09F293E04999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08B03623-A1C9-482F-B60E-09F293E04999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08B03623-A1C9-482F-B60E-09F293E04999}.Release|Any CPU.Build.0 = Release|Any CPU + {B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8C774E6-6B75-41AC-B3CF-10BD3623B2FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DD.Persistence/API/ISetpointApi.cs b/DD.Persistence/API/ISetpointApi.cs index f600f99..138e336 100644 --- a/DD.Persistence/API/ISetpointApi.cs +++ b/DD.Persistence/API/ISetpointApi.cs @@ -14,7 +14,7 @@ public interface ISetpointApi : ISyncApi /// ключи уставок /// /// - Task>> GetCurrent(IEnumerable setpoitKeys, CancellationToken token); + Task>> GetCurrent(IEnumerable setpoitKeys, CancellationToken token); /// /// Получить значения уставок за определенный момент времени diff --git a/DD.Persistence/API/ISyncApi.cs b/DD.Persistence/API/ISyncApi.cs index 5bd8379..ec8b9ba 100644 --- a/DD.Persistence/API/ISyncApi.cs +++ b/DD.Persistence/API/ISyncApi.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.API; diff --git a/DD.Persistence/API/ISyncWithDiscriminatorApi.cs b/DD.Persistence/API/ISyncWithDiscriminatorApi.cs index 05f0a90..f57504d 100644 --- a/DD.Persistence/API/ISyncWithDiscriminatorApi.cs +++ b/DD.Persistence/API/ISyncWithDiscriminatorApi.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.API; diff --git a/DD.Persistence/API/ITimeSeriesDataApi.cs b/DD.Persistence/API/ITimeSeriesDataApi.cs index d9b415a..1182455 100644 --- a/DD.Persistence/API/ITimeSeriesDataApi.cs +++ b/DD.Persistence/API/ITimeSeriesDataApi.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using DD.Persistence.Models; +using DD.Persistence.ModelsAbstractions; namespace DD.Persistence.API; @@ -7,7 +7,7 @@ namespace DD.Persistence.API; /// Интерфейс для работы с API временных данных /// public interface ITimeSeriesDataApi : ITimeSeriesBaseDataApi - where TDto : class, ITimeSeriesAbstractDto, new() + where TDto : class, ITimestampAbstractDto, new() { /// /// Получить список объектов, удовлетворяющий диапазон дат diff --git a/DD.Persistence/DD.Persistence.csproj b/DD.Persistence/DD.Persistence.csproj index f1c1782..781cf02 100644 --- a/DD.Persistence/DD.Persistence.csproj +++ b/DD.Persistence/DD.Persistence.csproj @@ -18,7 +18,10 @@ - + + + + diff --git a/DD.Persistence/EFExtensions.cs b/DD.Persistence/Extensions/EFExtensions.cs similarity index 97% rename from DD.Persistence/EFExtensions.cs rename to DD.Persistence/Extensions/EFExtensions.cs index a308db9..f1d1fb8 100644 --- a/DD.Persistence/EFExtensions.cs +++ b/DD.Persistence/Extensions/EFExtensions.cs @@ -2,7 +2,7 @@ using System.Linq.Expressions; using System.Reflection; -namespace DD.Persistence; +namespace DD.Persistence.Extensions; public static class EFExtensions { struct TypeAccessor @@ -23,7 +23,7 @@ public static class EFExtensions private static ConcurrentDictionary> TypePropSelectors { get; set; } = new(); private static MethodInfo GetExtOrderMethod(string methodName) - => typeof(System.Linq.Queryable) + => typeof(Queryable) .GetMethods() .Where(m => m.Name == methodName && m.IsGenericMethodDefinition && @@ -66,7 +66,7 @@ public static class EFExtensions /// и опционально указания направления сортировки "asc" или "desc" /// /// - /// var query = query("Date desc"); + /// var query = query("Timestamp desc"); /// /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( diff --git a/DD.Persistence/Extensions/IEnumerableExtensions.cs b/DD.Persistence/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..702a08b --- /dev/null +++ b/DD.Persistence/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,32 @@ +namespace DD.Persistence.Extensions; + +public static class IEnumerableExtensions +{ + public static void ForEach(this IEnumerable source, Action action) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (action == null) + throw new ArgumentNullException(nameof(action)); + + foreach (var item in source) + { + action(item); + } + } + + public static bool IsNullOrEmpty(this IEnumerable? enumerable) + { + if (enumerable == null) + { + return true; + } + + var collection = enumerable as ICollection; + if (collection != null) + { + return collection.Count < 1; + } + return !enumerable.Any(); + } +} diff --git a/DD.Persistence/Models/DataSaubDto.cs b/DD.Persistence/Models/DataSaubDto.cs deleted file mode 100644 index 34306d9..0000000 --- a/DD.Persistence/Models/DataSaubDto.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace DD.Persistence.Models; -public class DataSaubDto : ITimeSeriesAbstractDto -{ - public DateTimeOffset Date { get; set; } = DateTimeOffset.UtcNow; - - public int? Mode { get; set; } - - public string? User { get; set; } - - public double? WellDepth { get; set; } - - public double? BitDepth { get; set; } - - public double? BlockPosition { get; set; } - - public double? BlockSpeed { get; set; } - - public double? Pressure { get; set; } - - public double? AxialLoad { get; set; } - - public double? HookWeight { get; set; } - - public double? RotorTorque { get; set; } - - public double? RotorSpeed { get; set; } - - public double? Flow { get; set; } - - public short MseState { get; set; } - - public int IdFeedRegulator { get; set; } - - public double? Mse { get; set; } - - public double? Pump0Flow { get; set; } - - public double? Pump1Flow { get; set; } - - public double? Pump2Flow { get; set; } -} diff --git a/DD.Persistence/Models/TimestampedSetDto.cs b/DD.Persistence/Models/TimestampedSetDto.cs deleted file mode 100644 index cf72032..0000000 --- a/DD.Persistence/Models/TimestampedSetDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DD.Persistence.Models; - -/// -/// набор данных с отметкой времени -/// -/// отметка времени -/// набор данных -public record TimestampedSetDto(DateTimeOffset Timestamp, IDictionary Set); diff --git a/DD.Persistence/Repositories/IChangeLogRepository.cs b/DD.Persistence/Repositories/IChangeLogRepository.cs index 41dd379..6c39e24 100644 --- a/DD.Persistence/Repositories/IChangeLogRepository.cs +++ b/DD.Persistence/Repositories/IChangeLogRepository.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; namespace DD.Persistence.Repositories; diff --git a/DD.Persistence/Repositories/IDataSchemeRepository.cs b/DD.Persistence/Repositories/IDataSchemeRepository.cs new file mode 100644 index 0000000..c14a9cf --- /dev/null +++ b/DD.Persistence/Repositories/IDataSchemeRepository.cs @@ -0,0 +1,25 @@ +using DD.Persistence.Models; + +namespace DD.Persistence.Repositories; + +/// +/// Репозиторий для работы со схемами наборов данных +/// +public interface IDataSchemeRepository +{ + /// + /// Добавить схему + /// + /// + /// + /// + Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token); + + /// + /// Вычитать схему + /// + /// Идентификатор схемы + /// + /// + Task Get(Guid dataSchemeId, CancellationToken token); +} \ No newline at end of file diff --git a/DD.Persistence/Repositories/IDataSourceSystemRepository.cs b/DD.Persistence/Repositories/IDataSourceSystemRepository.cs index ce674d6..21d7b1c 100644 --- a/DD.Persistence/Repositories/IDataSourceSystemRepository.cs +++ b/DD.Persistence/Repositories/IDataSourceSystemRepository.cs @@ -3,19 +3,20 @@ namespace DD.Persistence.Repositories; /// -/// Интерфейс по работе с системами - источниками данных +/// Репозиторий для работы с системами - источниками данных /// public interface IDataSourceSystemRepository { - /// - /// Добавить систему - /// - /// - /// - public Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token); + /// + /// Добавить систему - источник данных + /// + /// + /// + /// + public Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token); /// - /// Получить список систем + /// Получить список систем - источников данных /// /// public Task> Get(CancellationToken token); diff --git a/DD.Persistence/Repositories/IParameterRepository.cs b/DD.Persistence/Repositories/IParameterRepository.cs index 53c48c3..7e8bbd2 100644 --- a/DD.Persistence/Repositories/IParameterRepository.cs +++ b/DD.Persistence/Repositories/IParameterRepository.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.Repositories; public interface IParameterRepository diff --git a/DD.Persistence/Repositories/ISetpointRepository.cs b/DD.Persistence/Repositories/ISetpointRepository.cs index 0af805d..7ece9cb 100644 --- a/DD.Persistence/Repositories/ISetpointRepository.cs +++ b/DD.Persistence/Repositories/ISetpointRepository.cs @@ -1,4 +1,6 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using System.Text.Json; namespace DD.Persistence.Repositories; @@ -15,6 +17,14 @@ public interface ISetpointRepository /// Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrentDictionary(IEnumerable setpointKeys, CancellationToken token); + /// /// Получить значения уставок за определенный момент времени /// @@ -58,5 +68,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token); + Task Add(Guid setpointKey, JsonElement newValue, Guid idUser, CancellationToken token); } diff --git a/DD.Persistence/Repositories/ISyncRepository.cs b/DD.Persistence/Repositories/ISyncRepository.cs deleted file mode 100644 index 10c21f2..0000000 --- a/DD.Persistence/Repositories/ISyncRepository.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DD.Persistence.Models; - -namespace DD.Persistence.Repositories; - -/// -/// Интерфейс по работе с данными -/// -/// -public interface ISyncRepository -{ - /// - /// Получить данные, начиная с определенной даты - /// - /// дата начала - /// /// - Task> GetGtDate(DateTimeOffset dateBegin, CancellationToken token); - - - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRange(CancellationToken token); -} diff --git a/DD.Persistence/Repositories/ISyncWithDiscriminatorRepository.cs b/DD.Persistence/Repositories/ISyncWithDiscriminatorRepository.cs index e761c8a..82efb39 100644 --- a/DD.Persistence/Repositories/ISyncWithDiscriminatorRepository.cs +++ b/DD.Persistence/Repositories/ISyncWithDiscriminatorRepository.cs @@ -1,4 +1,4 @@ -using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.Repositories; diff --git a/DD.Persistence/Repositories/ITechMessagesRepository.cs b/DD.Persistence/Repositories/ITechMessagesRepository.cs index 9909106..cd3c826 100644 --- a/DD.Persistence/Repositories/ITechMessagesRepository.cs +++ b/DD.Persistence/Repositories/ITechMessagesRepository.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; namespace DD.Persistence.Repositories @@ -19,6 +20,7 @@ namespace DD.Persistence.Repositories /// /// Добавление новых сообщений /// + /// /// /// /// diff --git a/DD.Persistence/Repositories/ITimeSeriesDataRepository.cs b/DD.Persistence/Repositories/ITimeSeriesDataRepository.cs deleted file mode 100644 index ed05980..0000000 --- a/DD.Persistence/Repositories/ITimeSeriesDataRepository.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DD.Persistence.Models; - -namespace DD.Persistence.Repositories; - -/// -/// Интерфейс по работе с временными данными -/// -/// -public interface ITimeSeriesDataRepository : ISyncRepository, ITimeSeriesBaseRepository - where TDto : class, ITimeSeriesAbstractDto, new() -{ - /// - /// Добавление записей - /// - /// - /// - /// - Task AddRange(IEnumerable dtos, CancellationToken token); -} diff --git a/DD.Persistence/Repositories/ITimestampedSetRepository.cs b/DD.Persistence/Repositories/ITimestampedValuesRepository.cs similarity index 50% rename from DD.Persistence/Repositories/ITimestampedSetRepository.cs rename to DD.Persistence/Repositories/ITimestampedValuesRepository.cs index cefbb3e..6bd2866 100644 --- a/DD.Persistence/Repositories/ITimestampedSetRepository.cs +++ b/DD.Persistence/Repositories/ITimestampedValuesRepository.cs @@ -1,17 +1,24 @@ using DD.Persistence.Models; +using DD.Persistence.RepositoriesAbstractions; namespace DD.Persistence.Repositories; /// -/// Репозиторий для хранения разных наборов данных рядов. -/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. -/// idDiscriminator формируют клиенты и только им известно что они обозначают. -/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// Репозиторий для работы с временными данными /// -public interface ITimestampedSetRepository +public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBaseRepository { /// - /// Количество записей по указанному набору в БД. Для пагинации. + /// Добавление записей + /// + /// Дискриминатор (идентификатор) набора + /// + /// + /// + Task AddRange(Guid idDiscriminator, IEnumerable dtos, CancellationToken token); + + /// + /// Количество записей по указанному набору в БД. Для пагинации /// /// Дискриминатор (идентификатор) набора /// @@ -28,32 +35,23 @@ public interface ITimestampedSetRepository /// /// /// - Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); + Task>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); /// - /// Диапазон дат за которые есть данные + /// Получение данных с начала /// - /// Дискриминатор (идентификатор) набора + /// Дискриминатор (идентификатор) набора + /// Количество /// /// - Task GetDatesRange(Guid idDiscriminator, CancellationToken token); + Task>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token); /// - /// Получить последние данные + /// Получение данных с конца /// - /// Дискриминатор (идентификатор) набора - /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора - /// + /// Дискриминатор (идентификатор) набора + /// Количество /// /// - Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token); - - /// - /// Добавление новых данных - /// - /// Дискриминатор (идентификатор) набора - /// - /// - /// - Task AddRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); -} \ No newline at end of file + Task>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token); +} diff --git a/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs b/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs new file mode 100644 index 0000000..95bfee2 --- /dev/null +++ b/DD.Persistence/RepositoriesAbstractions/ISyncRepository.cs @@ -0,0 +1,28 @@ +using DD.Persistence.Models; +using DD.Persistence.Models.Common; + +namespace DD.Persistence.RepositoriesAbstractions; + +/// +/// Интерфейс по работе с данными +/// +public interface ISyncRepository // ToDo: исчерпывающая абстракция +{ + /// + /// Получить данные, начиная с определенной даты + /// + /// + /// дата начала + /// + /// + Task>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token); + + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + /// + Task GetDatesRange(Guid discriminatorId, CancellationToken token); +} diff --git a/DD.Persistence/Repositories/ITimeSeriesBaseRepository.cs b/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs similarity index 56% rename from DD.Persistence/Repositories/ITimeSeriesBaseRepository.cs rename to DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs index 0d5e531..04b8e1a 100644 --- a/DD.Persistence/Repositories/ITimeSeriesBaseRepository.cs +++ b/DD.Persistence/RepositoriesAbstractions/ITimeSeriesBaseRepository.cs @@ -1,18 +1,23 @@ -namespace DD.Persistence.Repositories; +using DD.Persistence.Models; + +namespace DD.Persistence.RepositoriesAbstractions; /// /// Интерфейс по работе с прореженными данными /// -public interface ITimeSeriesBaseRepository - where TDto : class, new() +public interface ITimeSeriesBaseRepository // ToDo: исчерпывающая абстракция { /// /// Получить список объектов с прореживанием /// + /// /// дата начала + /// /// + /// /// - Task> GetResampledData( + Task>> GetResampledData( + Guid discriminatorId, DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, diff --git a/DD.Persistence/Services/Interfaces/ITimestampedValuesService.cs b/DD.Persistence/Services/Interfaces/ITimestampedValuesService.cs new file mode 100644 index 0000000..8e6d606 --- /dev/null +++ b/DD.Persistence/Services/Interfaces/ITimestampedValuesService.cs @@ -0,0 +1,85 @@ +using DD.Persistence.Models; +using DD.Persistence.Models.Common; + +namespace DD.Persistence.Services.Interfaces; + +/// +/// Сервис для работы с временными данными +/// +public interface ITimestampedValuesService +{ + /// + /// Добавление записей + /// + /// + /// + /// + /// + Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token); + + /// + /// Количество записей по указанному набору в БД. Для пагинации + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task Count(Guid discriminatorId, CancellationToken token); + + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + /// + Task> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); + + /// + /// Получение данных с начала + /// + /// Дискриминатор (идентификатор) набора + /// Количество + /// + /// + Task> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token); + + /// + /// Получить данные, начиная с определенной даты + /// + /// + /// дата начала + /// + /// + Task> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token); + + /// + /// Получение данных с конца + /// + /// Дискриминатор (идентификатор) набора + /// Количество + /// + /// + Task> GetLast(Guid discriminatorId, int takeCount, CancellationToken token); + + /// + /// Получить список объектов с прореживанием + /// + /// + /// дата начала + /// + /// + /// + /// + Task> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default); + + /// + /// Получить диапазон дат, для которых есть данные + /// + /// + /// + /// + Task GetDatesRange(Guid discriminatorId, CancellationToken token); +} \ No newline at end of file diff --git a/DD.Persistence/Services/Interfaces/IWitsDataService.cs b/DD.Persistence/Services/Interfaces/IWitsDataService.cs index 3a18da9..bb3e7d5 100644 --- a/DD.Persistence/Services/Interfaces/IWitsDataService.cs +++ b/DD.Persistence/Services/Interfaces/IWitsDataService.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; namespace DD.Persistence.Services.Interfaces; diff --git a/DD.Persistence/Services/TimestampedValuesService.cs b/DD.Persistence/Services/TimestampedValuesService.cs new file mode 100644 index 0000000..81a8fd3 --- /dev/null +++ b/DD.Persistence/Services/TimestampedValuesService.cs @@ -0,0 +1,199 @@ +using DD.Persistence.Extensions; +using DD.Persistence.Models; +using DD.Persistence.Models.Common; +using DD.Persistence.Repositories; +using DD.Persistence.Services.Interfaces; + +namespace DD.Persistence.Services; + +/// +public class TimestampedValuesService : ITimestampedValuesService +{ + private readonly ITimestampedValuesRepository timestampedValuesRepository; + private readonly IDataSchemeRepository dataSchemeRepository; + + /// + public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, IDataSchemeRepository relatedDataRepository) + { + this.timestampedValuesRepository = timestampedValuesRepository; + this.dataSchemeRepository = relatedDataRepository; + } + + /// + public async Task AddRange(Guid discriminatorId, IEnumerable dtos, CancellationToken token) + { + // ToDo: реализовать без foreach + foreach (var dto in dtos) + { + var keys = dto.Values.Keys.ToArray(); + await CreateSystemSpecificationIfNotExist(discriminatorId, keys, token); + } + + var result = await timestampedValuesRepository.AddRange(discriminatorId, dtos, token); + + return result; + } + + /// + public async Task> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) + { + var result = await timestampedValuesRepository.Get(discriminatorId, geTimestamp, columnNames, skip, take, token); + + var dtos = await Materialize(discriminatorId, result, token); + + if (!columnNames.IsNullOrEmpty()) + { + dtos = ReduceSetColumnsByNames(dtos, columnNames!); + } + + return dtos; + } + + /// + public async Task> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token) + { + var result = await timestampedValuesRepository.GetFirst(discriminatorId, takeCount, token); + + var dtos = await Materialize(discriminatorId, result, token); + + return dtos; + } + + /// + public async Task> GetLast(Guid discriminatorId, int takeCount, CancellationToken token) + { + var result = await timestampedValuesRepository.GetLast(discriminatorId, takeCount, token); + + var dtos = await Materialize(discriminatorId, result, token); + + return dtos; + } + + /// + public async Task> GetResampledData( + Guid discriminatorId, + DateTimeOffset beginTimestamp, + double intervalSec = 600d, + int approxPointsCount = 1024, + CancellationToken token = default) + { + var result = await timestampedValuesRepository.GetResampledData(discriminatorId, beginTimestamp, intervalSec, approxPointsCount, token); + + var dtos = await Materialize(discriminatorId, result, token); + + return dtos; + } + + /// + public async Task> GetGtDate(Guid discriminatorId, DateTimeOffset beginTimestamp, CancellationToken token) + { + var result = await timestampedValuesRepository.GetGtDate(discriminatorId, beginTimestamp, token); + + var dtos = await Materialize(discriminatorId, result, token); + + return dtos; + } + + /// + public async Task Count(Guid discriminatorId, CancellationToken token) + { + var result = await timestampedValuesRepository.Count(discriminatorId, token); + + return result; + } + + /// + public async virtual Task GetDatesRange(Guid discriminatorId, CancellationToken token) + { + var result = await timestampedValuesRepository.GetDatesRange(discriminatorId, token); + + return result; + } + + /// + /// Преобразовать результат запроса в набор dto + /// + /// + /// + /// + /// + private async Task> Materialize(Guid dataSchemeId, IEnumerable> queryResult, CancellationToken token) + { + var dataScheme = await dataSchemeRepository.Get(dataSchemeId, token); + if (dataScheme is null) + { + return []; + } + + var dtos = queryResult.Select(entity => + { + var dto = new TimestampedValuesDto() + { + Timestamp = entity.Item1.ToUniversalTime() + }; + + var identity = dataScheme!.PropNames; + var indexedIdentity = identity + .Select((value, index) => new { index, value }); + dto.Values = indexedIdentity + .ToDictionary(x => x.value, x => entity.Item2[x.index]); + + return dto; + }); + + return dtos; + } + + /// + /// Создать спецификацию, при отсутствии таковой + /// + /// Дискриминатор системы + /// Набор наименований полей + /// + /// + /// Некорректный набор наименований полей + private async Task CreateSystemSpecificationIfNotExist(Guid discriminatorId, string[] fieldNames, CancellationToken token) + { + var systemSpecification = await dataSchemeRepository.Get(discriminatorId, token); + if (systemSpecification is null) + { + systemSpecification = new DataSchemeDto() + { + DiscriminatorId = discriminatorId, + PropNames = fieldNames + }; + await dataSchemeRepository.Add(systemSpecification, token); + + return; + } + + if (!systemSpecification.PropNames.SequenceEqual(fieldNames)) + { + var expectedFieldNames = string.Join(", ", systemSpecification.PropNames); + var actualFieldNames = string.Join(", ", fieldNames); + throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " + + $"характерен набор данных: [{expectedFieldNames}], однако был передан набор: [{actualFieldNames}]"); + } + } + + /// + /// Отсеить лишние поля в соответствии с заданным фильтром + /// + /// + /// Поля, которые необходимо оставить + /// + private IEnumerable ReduceSetColumnsByNames(IEnumerable dtos, IEnumerable fieldNames) + { + var result = dtos.Select(dto => + { + var reducedValues = dto.Values + .Where(v => fieldNames.Contains(v.Key)) + .ToDictionary(); + dto.Values = reducedValues; + + return dto; + }); + + return result; + } +} diff --git a/DD.Persistence/Services/WitsDataService.cs b/DD.Persistence/Services/WitsDataService.cs index bcee812..6c6f0ee 100644 --- a/DD.Persistence/Services/WitsDataService.cs +++ b/DD.Persistence/Services/WitsDataService.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Configurations; using DD.Persistence.Models.Enumerations; using DD.Persistence.Repositories; diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..cffb4fe --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + <_Parameter1>$(AssemblyName).Test + + + <_Parameter1>DD.Persistence.IntegrationTests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + \ No newline at end of file