diff --git a/Persistence.API/Controllers/TimestampedSetController.cs b/Persistence.API/Controllers/TimestampedSetController.cs new file mode 100644 index 0000000..f18e4c8 --- /dev/null +++ b/Persistence.API/Controllers/TimestampedSetController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Repositories; +using System.Net; + +namespace 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 InsertRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable sets, CancellationToken token) + { + var result = await repository.InsertRange(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/Persistence.Client/Clients/ITimestampedSetClient.cs b/Persistence.Client/Clients/ITimestampedSetClient.cs new file mode 100644 index 0000000..95e8bd1 --- /dev/null +++ b/Persistence.Client/Clients/ITimestampedSetClient.cs @@ -0,0 +1,62 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.Client.Clients; + +/// +/// Клиент для работы с репозиторием для хранения разных наборов данных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// +public interface ITimestampedSetClient +{ + private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}"; + + /// + /// Добавление новых данных + /// + /// Дискриминатор (идентификатор) набора + /// + /// + [Post(baseUrl)] + Task> InsertRange(Guid idDiscriminator, IEnumerable sets); + + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + [Get(baseUrl)] + Task>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable? columnNames, int skip, int take); + + /// + /// Получить последние данные + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + [Get($"{baseUrl}/last")] + Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? columnNames, int take); + + /// + /// Количество записей по указанному набору в БД. Для пагинации. + /// + /// Дискриминатор (идентификатор) набора + /// + [Get($"{baseUrl}/count")] + Task> Count(Guid idDiscriminator); + + /// + /// Диапазон дат за которые есть данные + /// + /// Дискриминатор (идентификатор) набора + /// + [Get($"{baseUrl}/datesRange")] + Task> GetDatesRange(Guid idDiscriminator); +} diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index 58d201c..a0cae6a 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -1,17 +1,21 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; +using Persistence.Database.Entity; using System.Data.Common; namespace Persistence.Database.Model; -public partial class PersistenceDbContext : DbContext, IPersistenceDbContext +public partial class PersistenceDbContext : DbContext { public DbSet DataSaub => Set(); public DbSet Setpoint => Set(); + public DbSet TimestampedSets => Set(); + public PersistenceDbContext() : base() { - + } public PersistenceDbContext(DbContextOptions options) @@ -32,7 +36,9 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext { modelBuilder.HasPostgresExtension("adminpack") .HasAnnotation("Relational:Collation", "Russian_Russia.1251"); + + modelBuilder.Entity() + .Property(e => e.Set) + .HasJsonConversion(); } - - } diff --git a/Persistence.Database/EFExtensions.cs b/Persistence.Database/EFExtensions.cs new file mode 100644 index 0000000..d60d768 --- /dev/null +++ b/Persistence.Database/EFExtensions.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace Persistence.Database; + +public static class EFExtensions +{ + private static readonly JsonSerializerOptions jsonSerializerOptions = new() + { + AllowTrailingCommas = true, + WriteIndented = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + }; + + public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasJsonConversion( + this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder) + => HasJsonConversion(builder, jsonSerializerOptions); + + public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasJsonConversion( + this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder, + JsonSerializerOptions jsonSerializerOptions) + { + builder.HasConversion( + s => JsonSerializer.Serialize(s, jsonSerializerOptions), + s => JsonSerializer.Deserialize(s, jsonSerializerOptions)!); + + ValueComparer valueComparer = new( + (a, b) => + (a != null) && (b != null) + ? a.GetHashCode() == b.GetHashCode() + : (a == null) && (b == null), + i => (i == null) ? -1 : i.GetHashCode(), + i => i); + + builder.Metadata.SetValueComparer(valueComparer); + return builder; + } + +} + diff --git a/Persistence.Database/Entity/TimestampedSet.cs b/Persistence.Database/Entity/TimestampedSet.cs new file mode 100644 index 0000000..c9a0dda --- /dev/null +++ b/Persistence.Database/Entity/TimestampedSet.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; + +namespace 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/Persistence.Database/IPersistenceDbContext.cs b/Persistence.Database/IPersistenceDbContext.cs deleted file mode 100644 index 66f34ff..0000000 --- a/Persistence.Database/IPersistenceDbContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Persistence.Database.Model; - -namespace Persistence.Database; -public interface IPersistenceDbContext : IDisposable -{ - DbSet DataSaub { get; } -} diff --git a/Persistence.Database/Model/IPersistenceDbContext.cs b/Persistence.Database/Model/IPersistenceDbContext.cs deleted file mode 100644 index 2c1aebb..0000000 --- a/Persistence.Database/Model/IPersistenceDbContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Persistence.Database.Model; -public interface IPersistenceDbContext : IDisposable -{ - DbSet DataSaub { get; } - DbSet Setpoint { get; } - DatabaseFacade Database { get; } - Task SaveChangesAsync(CancellationToken cancellationToken); -} diff --git a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs new file mode 100644 index 0000000..aa33e1b --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs @@ -0,0 +1,222 @@ +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; +using Persistence.Models; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers; +public class TimestampedSetControllerTest : BaseIntegrationTest +{ + private readonly ITimestampedSetClient client; + + public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory) + { + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); + + client = persistenceClientFactory.GetClient(); + } + + [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.InsertRange(idDiscriminator, testSets); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal(testSets.Count(), response.Content); + } + + [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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Get(idDiscriminator, null, null, 0, int.MaxValue); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(count, items.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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + string[] props = ["A"]; + + // act + var response = await client.Get(idDiscriminator, null, props, 0, int.MaxValue); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(count, items.Count()); + foreach ( var item in items ) + { + 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.InsertRange(idDiscriminator, testSets); + 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); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + var minDate = items.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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var expectedCount = count / 2; + + // act + var response = await client.Get(idDiscriminator, null, null, 2, expectedCount); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Get(idDiscriminator, null, null, count - expectedCount, count); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + } + + [Fact] + public async Task GetLast() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var expectedCount = 8; + + // act + var response = await client.GetLast(idDiscriminator, null, expectedCount); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var tolerance = TimeSpan.FromSeconds(1); + + // act + var response = await client.GetDatesRange(idDiscriminator); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var range = response.Content!; + Assert.Equal(dateMin, range.From, tolerance); + Assert.Equal(dateMax, range.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))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Count(idDiscriminator); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal(count, response.Content); + } + + 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/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index b5f6f97..7b12362 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Persistence.API; +using Persistence.Database; using Persistence.Client; using Persistence.Database.Model; using Persistence.Database.Postgres; diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 27063cb..e79c555 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -17,6 +17,8 @@ public static class DependencyInjection services.AddTransient, TimeSeriesDataRepository>(); services.AddTransient(); + services.AddTransient, TimeSeriesDataCachedRepository>(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs new file mode 100644 index 0000000..ad9a6cf --- /dev/null +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -0,0 +1,121 @@ +using Microsoft.EntityFrameworkCore; +using Persistence.Database.Entity; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.Repository.Repositories; + +/// +/// Репозиторий для хранения разных наборов данных временных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// +public class TimestampedSetRepository : ITimestampedSetRepository +{ + private readonly DbContext db; + + public TimestampedSetRepository(DbContext db) + { + this.db = db; + } + + public Task InsertRange(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/Persistence/Models/TimestampedSetDto.cs b/Persistence/Models/TimestampedSetDto.cs new file mode 100644 index 0000000..3235a4e --- /dev/null +++ b/Persistence/Models/TimestampedSetDto.cs @@ -0,0 +1,8 @@ +namespace Persistence.Models; + +/// +/// набор данных с отметкой времени +/// +/// отметка времени +/// набор данных +public record TimestampedSetDto(DateTimeOffset Timestamp, IDictionary Set); diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs new file mode 100644 index 0000000..27627c3 --- /dev/null +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -0,0 +1,59 @@ +using Persistence.Models; + +namespace Persistence.Repositories; + +/// +/// Репозиторий для хранения разных наборов данных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// +public interface ITimestampedSetRepository +{ + /// + /// Количество записей по указанному набору в БД. Для пагинации. + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task Count(Guid idDiscriminator, CancellationToken token); + + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + /// + Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); + + /// + /// Диапазон дат за которые есть данные + /// + /// Дискриминатор (идентификатор) набора + /// + /// + Task GetDatesRange(Guid idDiscriminator, CancellationToken token); + + /// + /// Получить последние данные + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token); + + /// + /// Добавление новых данных + /// + /// Дискриминатор (идентификатор) набора + /// + /// + /// + Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); +} \ No newline at end of file