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 0850438..2f1c6fc 100644 --- a/DD.Persistence.API/Controllers/SetpointController.cs +++ b/DD.Persistence.API/Controllers/SetpointController.cs @@ -4,6 +4,7 @@ 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; 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 6b0754c..b543841 100644 --- a/DD.Persistence.API/DependencyInjection.cs +++ b/DD.Persistence.API/DependencyInjection.cs @@ -53,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.App/appsettings.Tests.json b/DD.Persistence.App/appsettings.Tests.json index 9934757..2c890bb 100644 --- a/DD.Persistence.App/appsettings.Tests.json +++ b/DD.Persistence.App/appsettings.Tests.json @@ -1,10 +1,10 @@ { "DbConnection": { - "Host": "localhost", + "Host": "postgres", "Port": 5432, "Database": "persistence", "Username": "postgres", - "Password": "postgres" + "Password": "q" }, "NeedUseKeyCloak": false, "AuthUser": { diff --git a/DD.Persistence.Client/Clients/ChangeLogClient.cs b/DD.Persistence.Client/Clients/ChangeLogClient.cs index 38c2ec4..e4f5904 100644 --- a/DD.Persistence.Client/Clients/ChangeLogClient.cs +++ b/DD.Persistence.Client/Clients/ChangeLogClient.cs @@ -4,6 +4,7 @@ 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 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 f197fe8..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; 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 db619e5..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, ITimeSeriesAbstractDto -{ - /// - /// Добавление записей - /// - /// - /// - /// - 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 432cccb..0000000 --- a/DD.Persistence.Client/Clients/Interfaces/ITimestampedSetClient.cs +++ /dev/null @@ -1,82 +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); - - /// - /// Получение данных с фильтрацией. Значение фильтра 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); - - /// - /// - /// - /// - /// - /// - /// - /// - 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 83c240f..8e35b4e 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitChangeLogClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using DD.Persistence.Models.Requests; using Refit; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs index 9568a51..4a140e0 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitClient.cs @@ -5,6 +5,10 @@ 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/IRefitSetpointClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs index 1cd2742..7931e6d 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using Refit; using System.Text.Json; diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs index 337911e..ef9496e 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitTechMessagesClient.cs @@ -1,6 +1,7 @@ using DD.Persistence.Models; using DD.Persistence.Models.Requests; using Refit; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients.Interfaces.Refit { 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 832cfde..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 : IRefitClient, IDisposable - where TDto : class, ITimeSeriesAbstractDto -{ - 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 6211d2d..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 : IRefitClient, 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 dcf2021..233083a 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitWitsDataClient.cs @@ -1,5 +1,6 @@ using DD.Persistence.Models; using Refit; +using DD.Persistence.Models.Common; namespace DD.Persistence.Client.Clients.Interfaces.Refit; public interface IRefitWitsDataClient : IRefitClient, IDisposable diff --git a/DD.Persistence.Client/Clients/SetpointClient.cs b/DD.Persistence.Client/Clients/SetpointClient.cs index 0e8df6a..dfb5026 100644 --- a/DD.Persistence.Client/Clients/SetpointClient.cs +++ b/DD.Persistence.Client/Clients/SetpointClient.cs @@ -1,4 +1,4 @@ -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; @@ -6,6 +6,7 @@ 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; diff --git a/DD.Persistence.Client/Clients/TechMessagesClient.cs b/DD.Persistence.Client/Clients/TechMessagesClient.cs index e23c961..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; diff --git a/DD.Persistence.Client/Clients/TimeSeriesClient.cs b/DD.Persistence.Client/Clients/TimeSeriesClient.cs deleted file mode 100644 index c75ec81..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, ITimeSeriesAbstractDto -{ - private readonly IRefitTimeSeriesClient timeSeriesClient; - - public TimeSeriesClient(IRefitClientFactory> refitTechMessagesClientFactory, ILogger> logger) : base(logger) - { - this.timeSeriesClient = refitTechMessagesClientFactory.Create(); - } - - 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 6295d02..0000000 --- a/DD.Persistence.Client/Clients/TimestampedSetClient.cs +++ /dev/null @@ -1,92 +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; -using static System.Runtime.InteropServices.JavaScript.JSType; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Reflection; -using System.Collections.Concurrent; - -namespace DD.Persistence.Client.Clients; -public class TimestampedSetClient : BaseClient, ITimestampedSetClient -{ - private readonly IRefitTimestampedSetClient refitTimestampedSetClient; - private readonly ConcurrentDictionary mapperCache = new(); - public TimestampedSetClient(IRefitClientFactory refitTimestampedSetClientFactory, ILogger logger) : base(logger) - { - this.refitTimestampedSetClient = refitTimestampedSetClientFactory.Create(); - } - - 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 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, IEnumerable? columnNames, int take, CancellationToken token) - { - var data = await GetLast(idDiscriminator, columnNames, 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/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 e2703dc..d47f771 100644 --- a/DD.Persistence.Client/Clients/WitsDataClient.cs +++ b/DD.Persistence.Client/Clients/WitsDataClient.cs @@ -3,6 +3,7 @@ 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 diff --git a/DD.Persistence.Client/DependencyInjection.cs b/DD.Persistence.Client/DependencyInjection.cs index e5c7c29..89f65cf 100644 --- a/DD.Persistence.Client/DependencyInjection.cs +++ b/DD.Persistence.Client/DependencyInjection.cs @@ -22,8 +22,7 @@ public static class DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient, TimeSeriesClient>(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddSingleton(provider => 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/TimestampedSetMapper.cs b/DD.Persistence.Client/TimestampedSetMapper.cs index 521fc18..cf7463a 100644 --- a/DD.Persistence.Client/TimestampedSetMapper.cs +++ b/DD.Persistence.Client/TimestampedSetMapper.cs @@ -8,7 +8,7 @@ namespace DD.Persistence.Client; internal abstract class TimestampedSetMapperBase { - public abstract object Map(TimestampedSetDto data); + public abstract object Map(TimestampedValuesDto data); } internal class TimestampedSetMapper : TimestampedSetMapperBase @@ -22,12 +22,12 @@ internal class TimestampedSetMapper : TimestampedSetMapperBase IdDiscriminator = idDiscriminator; } - public override object Map(TimestampedSetDto data) + public override object Map(TimestampedValuesDto data) { return DeserializeTimeStampedData(data)!; } - public T DeserializeTimeStampedData(TimestampedSetDto data) + public T DeserializeTimeStampedData(TimestampedValuesDto data) { if (entityType.IsValueType) @@ -36,10 +36,10 @@ internal class TimestampedSetMapper : TimestampedSetMapperBase return MapClass(data); } - private T MapClass(TimestampedSetDto data) + private T MapClass(TimestampedValuesDto data) { var entity = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); - foreach (var (propertyName, value) in data.Set) + foreach (var (propertyName, value) in data.Values) { if (value is JsonElement jsonElement) SetPropertyValueFromJson(ref entity, propertyName, jsonElement); @@ -48,11 +48,11 @@ internal class TimestampedSetMapper : TimestampedSetMapperBase return entity; } - private T MapStruct(TimestampedSetDto data) + private T MapStruct(TimestampedValuesDto data) { var entity = Activator.CreateInstance(); object boxedEntity = entity!; - foreach (var (propertyName, value) in data.Set) + foreach (var (propertyName, value) in data.Values) { if (value is JsonElement jsonElement) SetPropertyValueForStructFromJson(ref boxedEntity, propertyName, jsonElement); diff --git a/DD.Persistence.Database.Postgres/Migrations/20241220062251_Init.Designer.cs b/DD.Persistence.Database.Postgres/Migrations/20250122111321_Init.Designer.cs similarity index 66% rename from DD.Persistence.Database.Postgres/Migrations/20241220062251_Init.Designer.cs rename to DD.Persistence.Database.Postgres/Migrations/20250122111321_Init.Designer.cs index 02a25ce..a3678d6 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20241220062251_Init.Designer.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250122111321_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("20241220062251_Init")] + [Migration("20250122111321_Init")] partial class Init { /// @@ -20,11 +21,28 @@ namespace DD.Persistence.Database.Postgres.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); 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("DataSchemes"); + }); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => { b.Property("SystemId") @@ -105,27 +123,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("TechMessage"); }); - 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("TimestampedSets", t => - { - t.HasComment("Общая таблица данных временных рядов"); - }); + b.ToTable("TimestampedValues"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -181,96 +196,13 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("ChangeLog"); }); - 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("DataSaub"); - }); - 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/20241220062251_Init.cs b/DD.Persistence.Database.Postgres/Migrations/20250122111321_Init.cs similarity index 70% rename from DD.Persistence.Database.Postgres/Migrations/20241220062251_Init.cs rename to DD.Persistence.Database.Postgres/Migrations/20250122111321_Init.cs index 03a8c43..6f5ecfc 100644 --- a/DD.Persistence.Database.Postgres/Migrations/20241220062251_Init.cs +++ b/DD.Persistence.Database.Postgres/Migrations/20250122111321_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: "DataSaub", + name: "DataSchemes", 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_DataSaub", x => x.date); + table.PrimaryKey("PK_DataSchemes", 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: "TimestampedSets", + name: "TimestampedValues", 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_TimestampedSets", x => new { x.IdDiscriminator, x.Timestamp }); - }, - comment: "Общая таблица данных временных рядов"); + table.PrimaryKey("PK_TimestampedValues", x => new { x.DiscriminatorId, x.Timestamp }); + table.ForeignKey( + name: "FK_TimestampedValues_DataSchemes_DiscriminatorId", + column: x => x.DiscriminatorId, + principalTable: "DataSchemes", + principalColumn: "DiscriminatorId", + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateTable( name: "TechMessage", @@ -150,9 +139,6 @@ namespace DD.Persistence.Database.Postgres.Migrations migrationBuilder.DropTable( name: "ChangeLog"); - migrationBuilder.DropTable( - name: "DataSaub"); - migrationBuilder.DropTable( name: "ParameterData"); @@ -163,10 +149,13 @@ namespace DD.Persistence.Database.Postgres.Migrations name: "TechMessage"); migrationBuilder.DropTable( - name: "TimestampedSets"); + name: "TimestampedValues"); migrationBuilder.DropTable( name: "DataSourceSystem"); + + migrationBuilder.DropTable( + name: "DataSchemes"); } } } diff --git a/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs b/DD.Persistence.Database.Postgres/Migrations/PersistencePostgresContextModelSnapshot.cs index 4c66e90..7171bb8 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; @@ -17,11 +18,28 @@ namespace DD.Persistence.Database.Postgres.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); 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("DataSchemes"); + }); + modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => { b.Property("SystemId") @@ -102,27 +120,24 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("TechMessage"); }); - 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("TimestampedSets", t => - { - t.HasComment("Общая таблица данных временных рядов"); - }); + b.ToTable("TimestampedValues"); }); modelBuilder.Entity("DD.Persistence.Database.Model.ChangeLog", b => @@ -178,96 +193,13 @@ namespace DD.Persistence.Database.Postgres.Migrations b.ToTable("ChangeLog"); }); - 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("DataSaub"); - }); - 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 439e886..d874ea2 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 d4515b2..0000000 --- a/DD.Persistence.Database/Entity/DataSaub.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DD.Persistence.Database.Model; -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..7fa3394 --- /dev/null +++ b/DD.Persistence.Database/Entity/DataScheme.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace DD.Persistence.Database.Entity; + +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 d200731..cc282ac 100644 --- a/DD.Persistence.Database/Entity/DataSourceSystem.cs +++ b/DD.Persistence.Database/Entity/DataSourceSystem.cs @@ -9,7 +9,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 c81b029..e1e1a5c 100644 --- a/DD.Persistence.Database/Entity/ParameterData.cs +++ b/DD.Persistence.Database/Entity/ParameterData.cs @@ -1,11 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using DD.Persistence.Database.EntityAbstractions; +using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace DD.Persistence.Database.Entity; [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 75bca4f..e526c22 100644 --- a/DD.Persistence.Database/Entity/Setpoint.cs +++ b/DD.Persistence.Database/Entity/Setpoint.cs @@ -1,11 +1,12 @@ -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 { - [PrimaryKey(nameof(Key), nameof(Created))] - public class Setpoint + [PrimaryKey(nameof(Key), nameof(Timestamp))] + public class Setpoint : ITimestampedItem { [Comment("Ключ")] public Guid Key { get; set; } @@ -14,7 +15,7 @@ namespace DD.Persistence.Database.Model 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 e233941..ef17e60 100644 --- a/DD.Persistence.Database/Entity/TechMessage.cs +++ b/DD.Persistence.Database/Entity/TechMessage.cs @@ -1,10 +1,11 @@ +using DD.Persistence.Database.EntityAbstractions; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace DD.Persistence.Database.Entity { - 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 f0d1815..0000000 --- a/DD.Persistence.Database/Entity/TimestampedSet.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations.Schema; - -namespace DD.Persistence.Database.Entity; - -[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..28c8946 --- /dev/null +++ b/DD.Persistence.Database/Entity/TimestampedValues.cs @@ -0,0 +1,22 @@ +using DD.Persistence.Database.EntityAbstractions; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace DD.Persistence.Database.Entity; + +[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/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/SetpointControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index c27f6cc..e84402a 100644 --- a/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,13 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; using DD.Persistence.Client; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Database.Model; -using System.Net; -using Xunit; -using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients; +using DD.Persistence.Client.Clients.Interfaces; +using DD.Persistence.Client.Clients.Interfaces.Refit; +using DD.Persistence.Database.Model; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Text.Json; +using Xunit; namespace DD.Persistence.IntegrationTests.Controllers { @@ -15,12 +14,6 @@ namespace DD.Persistence.IntegrationTests.Controllers { private readonly ISetpointClient setpointClient; private readonly SetpointConfigStorage configStorage; - - private class TestObject - { - public string? Value1 { get; set; } - public int? Value2 { get; set; } - } public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { var refitClientFactory = scope.ServiceProvider @@ -184,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); @@ -195,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); } @@ -247,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 51afc02..7974470 100644 --- a/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/TechMessagesControllerTest.cs @@ -15,7 +15,7 @@ namespace DD.Persistence.IntegrationTests.Controllers { public class TechMessagesControllerTest : BaseIntegrationTest { - private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DataSourceSystem).FullName}CacheKey"; + private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey"; private readonly ITechMessagesClient techMessagesClient; private readonly IMemoryCache memoryCache; public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) diff --git a/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs deleted file mode 100644 index e8f05ce..0000000 --- a/DD.Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs +++ /dev/null @@ -1,122 +0,0 @@ -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 DD.Persistence.Models; -using Mapster; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit; - -namespace DD.Persistence.IntegrationTests.Controllers; - -public abstract class TimeSeriesBaseControllerTest : BaseIntegrationTest - where TEntity : class, ITimestampedData, new() - where TDto : class, ITimeSeriesAbstractDto, new() -{ - private readonly ITimeSeriesClient timeSeriesClient; - - public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory) - { - dbContext.CleanupDbSet(); - - var refitClientFactory = scope.ServiceProvider - .GetRequiredService>>(); - var logger = scope.ServiceProvider.GetRequiredService>>(); - - timeSeriesClient = scope.ServiceProvider - .GetRequiredService>(); - } - - 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 1e84e9d..0000000 --- a/DD.Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs +++ /dev/null @@ -1,211 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using DD.Persistence.Client; -using DD.Persistence.Client.Clients.Interfaces; -using DD.Persistence.Models; -using Xunit; -using DD.Persistence.Client.Clients.Interfaces.Refit; -using DD.Persistence.Client.Clients; -using Microsoft.Extensions.Logging; - -namespace DD.Persistence.IntegrationTests.Controllers; -public class TimestampedSetControllerTest : BaseIntegrationTest -{ - private readonly ITimestampedSetClient client; - - public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory) - { - var refitClientFactory = scope.ServiceProvider - .GetRequiredService>(); - var logger = scope.ServiceProvider.GetRequiredService>(); - - client = scope.ServiceProvider - .GetRequiredService(); - } - - [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.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/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 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/DatesRangeDto.cs b/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/ITimeSeriesAbstractDto.cs b/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 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/PaginationContainer.cs b/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/SetpointLogDto.cs b/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/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.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.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/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 30e67d5..8c3ae65 100644 --- a/DD.Persistence.Repository/Repositories/SetpointRepository.cs +++ b/DD.Persistence.Repository/Repositories/SetpointRepository.cs @@ -1,9 +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 { @@ -26,7 +27,7 @@ namespace DD.Persistence.Repository.Repositories var entities = await query .Where(e => setpointKeys.Contains(e.Key)) .GroupBy(e => e.Key) - .Select(g => g.OrderByDescending(x => x.Created).FirstOrDefault()) + .Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault()) .ToArrayAsync(token); var dtos = entities.Select(e => e.Adapt()); @@ -39,7 +40,7 @@ namespace DD.Persistence.Repository.Repositories var entities = await query .Where(e => setpointKeys.Contains(e.Key)) .GroupBy(e => e.Key) - .Select(g => g.OrderByDescending(x => x.Created).FirstOrDefault()) + .Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault()) .ToDictionaryAsync(x=> x.Key, x => (object)x.Value, token); return entities; @@ -53,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()); @@ -65,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 @@ -80,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() @@ -113,7 +114,7 @@ namespace DD.Persistence.Repository.Repositories Key = setpointKey, Value = newValue, IdUser = idUser, - Created = DateTimeOffset.UtcNow + Timestamp = DateTimeOffset.UtcNow.ToUniversalTime() }; await db.Set().AddAsync(entity, token); diff --git a/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs b/DD.Persistence.Repository/Repositories/TechMessagesRepository.cs index 0bce538..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; 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 eeb198c..03f0be9 100644 --- a/DD.Persistence.sln +++ b/DD.Persistence.sln @@ -28,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt 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 @@ -74,6 +76,10 @@ Global {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/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/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/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 165ce29..7ece9cb 100644 --- a/DD.Persistence/Repositories/ISetpointRepository.cs +++ b/DD.Persistence/Repositories/ISetpointRepository.cs @@ -1,4 +1,5 @@ using DD.Persistence.Models; +using DD.Persistence.Models.Common; using System.Text.Json; namespace DD.Persistence.Repositories; 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 638f341..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 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;