Правки по результатам ревью #1

This commit is contained in:
Roman Efremov 2025-01-20 17:11:44 +05:00
parent d8497ce46a
commit 16a14e7e19
29 changed files with 580 additions and 222 deletions

View File

@ -14,9 +14,9 @@ namespace DD.Persistence.API.Controllers;
[Route("api/[controller]")]
public class DataSourceSystemController : ControllerBase
{
private readonly IRelatedDataRepository<DataSourceSystemDto> dataSourceSystemRepository;
private readonly IDataSourceSystemRepository dataSourceSystemRepository;
public DataSourceSystemController(IRelatedDataRepository<DataSourceSystemDto> dataSourceSystemRepository)
public DataSourceSystemController(IDataSourceSystemRepository dataSourceSystemRepository)
{
this.dataSourceSystemRepository = dataSourceSystemRepository;
}

View File

@ -1,6 +1,7 @@
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
@ -10,13 +11,13 @@ namespace DD.Persistence.API.Controllers;
/// Хранение наборов данных с отметкой времени.
/// </summary>
[ApiController]
//[Authorize]
[Authorize]
[Route("api/[controller]/{discriminatorId}")]
public class TimestampedValuesController : ControllerBase
{
private readonly ITimestampedValuesRepository timestampedValuesRepository;
private readonly ITimestampedValuesService timestampedValuesRepository;
public TimestampedValuesController(ITimestampedValuesRepository repository)
public TimestampedValuesController(ITimestampedValuesService repository)
{
this.timestampedValuesRepository = repository;
}
@ -47,6 +48,8 @@ public class TimestampedValuesController : ControllerBase
/// <param name="take"></param>
/// <param name="token"></param>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> 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);
@ -61,6 +64,8 @@ public class TimestampedValuesController : ControllerBase
/// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="token"></param>
[HttpGet("gtdate")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetGtDate([FromRoute] Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{
var result = await timestampedValuesRepository.GetGtDate(discriminatorId, timestampBegin, token);
@ -75,6 +80,8 @@ public class TimestampedValuesController : ControllerBase
/// <param name="take"></param>
/// <param name="token"></param>
[HttpGet("first")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetFirst([FromRoute] Guid discriminatorId, int take, CancellationToken token)
{
var result = await timestampedValuesRepository.GetFirst(discriminatorId, take, token);
@ -89,6 +96,8 @@ public class TimestampedValuesController : ControllerBase
/// <param name="take"></param>
/// <param name="token"></param>
[HttpGet("last")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> GetLast([FromRoute] Guid discriminatorId, int take, CancellationToken token)
{
var result = await timestampedValuesRepository.GetLast(discriminatorId, take, token);
@ -105,6 +114,8 @@ public class TimestampedValuesController : ControllerBase
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
[HttpGet("resampled")]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> 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);

View File

@ -53,6 +53,7 @@ public static class DependencyInjection
public static void AddServices(this IServiceCollection services)
{
services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
}
#region Authentication

View File

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

View File

@ -18,5 +18,5 @@ public class TimestampedValues : ITimestampedItem
public required object[] Values { get; set; }
[Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")]
public virtual ValuesIdentity? ValuesIdentity { get; set; }
public virtual DataScheme? DataScheme { get; set; }
}

View File

@ -11,7 +11,7 @@ public class PersistenceDbContext : DbContext
{
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<ValuesIdentity> ValuesIdentities => Set<ValuesIdentity>();
public DbSet<DataScheme> DataSchemes => Set<DataScheme>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
@ -31,8 +31,8 @@ public class PersistenceDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ValuesIdentity>()
.Property(e => e.Identity)
modelBuilder.Entity<DataScheme>()
.Property(e => e.PropNames)
.HasJsonConversion();
modelBuilder.Entity<TimestampedValues>()

View File

@ -13,9 +13,9 @@ using Xunit;
namespace DD.Persistence.IntegrationTests.Controllers;
public class TimestampedValuesControllerTest : BaseIntegrationTest
{
private static readonly string SystemCacheKey = $"{typeof(ValuesIdentity).FullName}CacheKey";
private readonly ITimestampedValuesClient timestampedValuesClient;
private readonly IMemoryCache memoryCache;
private IEnumerable<Guid> discriminatorIds = [];
public TimestampedValuesControllerTest(WebAppFactoryFixture factory) : base(factory)
{
@ -32,6 +32,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
public async Task AddRange_returns_success()
{
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
await AddRange(discriminatorId);
}
@ -43,6 +44,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
//act
var response = await timestampedValuesClient.Get(discriminatorId, null, null, 0, 1, CancellationToken.None);
@ -59,6 +61,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
var columnNames = new List<string>() { "A", "C" };
var skip = 5;
@ -94,6 +98,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
//act
@ -109,6 +115,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var dtos = await AddRange(discriminatorId);
var timestampBegin = DateTimeOffset.UtcNow.AddSeconds(-5);
@ -130,6 +138,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var take = 1;
//act
@ -145,6 +155,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var dtos = await AddRange(discriminatorId);
var take = 1;
@ -172,7 +184,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var take = 1;
//act
@ -187,7 +202,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var dtos = await AddRange(discriminatorId);
var take = 1;
@ -215,7 +233,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var timestampBegin = DateTimeOffset.UtcNow;
//act
@ -230,7 +251,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var count = 2048;
var timestampBegin = DateTimeOffset.UtcNow;
var dtos = await AddRange(discriminatorId, count);
@ -253,7 +277,9 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
//act
var response = await timestampedValuesClient.Count(discriminatorId, CancellationToken.None);
@ -267,7 +293,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var dtos = await AddRange(discriminatorId);
//act
@ -283,7 +312,9 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
//act
var response = await timestampedValuesClient.GetDatesRange(discriminatorId, CancellationToken.None);
@ -297,7 +328,10 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
{
//arrange
Cleanup();
var discriminatorId = Guid.NewGuid();
discriminatorIds.Append(discriminatorId);
var dtos = await AddRange(discriminatorId);
//act
@ -364,8 +398,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup()
{
memoryCache.Remove(SystemCacheKey);
discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<ValuesIdentity>();
dbContext.CleanupDbSet<DataScheme>();
}
}

View File

@ -0,0 +1,17 @@
namespace DD.Persistence.Models;
/// <summary>
/// Схема для набора данных
/// </summary>
public class DataSchemeDto
{
/// <summary>
/// Дискриминатор
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Наименования полей
/// </summary>
public string[] PropNames { get; set; } = [];
}

View File

@ -1,17 +0,0 @@
namespace DD.Persistence.Models;
/// <summary>
/// Набор идентификаторов для набора данных
/// </summary>
public class ValuesIdentityDto
{
/// <summary>
/// Дискриминатор системы
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Идентификаторы
/// </summary>
public string[] Identity { get; set; } = [];
}

View File

@ -41,10 +41,8 @@ public static class DependencyInjection
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IRelatedDataRepository<DataSourceSystemDto>,
RelatedDataCachedRepository<DataSourceSystemDto, DataSourceSystem>>();
services.AddTransient<IRelatedDataRepository<ValuesIdentityDto>,
RelatedDataCachedRepository<ValuesIdentityDto, ValuesIdentity>>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<IDataSchemeRepository, DataSchemeCachedRepository>();
return services;
}

View File

@ -3,6 +3,7 @@ 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;

View File

@ -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<DataScheme> GetQueryReadOnly() => db.Set<DataScheme>();
public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataScheme>();
await db.Set<DataScheme>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> GetByDiscriminator(Guid discriminatorId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId);
var entity = await query.ToArrayAsync();
var dto = entity.Select(e => e.Adapt<DataSchemeDto>()).FirstOrDefault();
return dto;
}
}

View File

@ -0,0 +1,33 @@
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 DataSourceSystemRepository : IDataSourceSystemRepository
{
protected DbContext db;
public DataSourceSystemRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataSourceSystem>();
await db.Set<DataSourceSystem>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<IEnumerable<DataSourceSystemDto>> Get(CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<DataSourceSystemDto>());
return dtos;
}
}

View File

@ -1,33 +0,0 @@
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class RelatedDataRepository<TDto, TEntity> : IRelatedDataRepository<TDto>
where TDto : class, new()
where TEntity : class, new()
{
protected DbContext db;
public RelatedDataRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<TEntity> GetQueryReadOnly() => db.Set<TEntity>();
public virtual async Task Add(TDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<TEntity>();
await db.Set<TEntity>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<IEnumerable<TDto>> Get(CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<TDto>());
return dtos;
}
}

View File

@ -1,4 +1,5 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Extensions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
@ -10,10 +11,10 @@ namespace DD.Persistence.Repository.Repositories
{
public class TechMessagesRepository : ITechMessagesRepository
{
private readonly IRelatedDataRepository<DataSourceSystemDto> sourceSystemRepository;
private readonly IDataSourceSystemRepository sourceSystemRepository;
private DbContext db;
public TechMessagesRepository(DbContext db, IRelatedDataRepository<DataSourceSystemDto> sourceSystemRepository)
public TechMessagesRepository(DbContext db, IDataSourceSystemRepository sourceSystemRepository)
{
this.db = db;
this.sourceSystemRepository = sourceSystemRepository;

View File

@ -2,32 +2,25 @@
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using DD.Persistence.Repository.Extensions;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository
{
private readonly DbContext db;
private readonly IRelatedDataRepository<ValuesIdentityDto> relatedDataRepository;
public TimestampedValuesRepository(DbContext db, IRelatedDataRepository<ValuesIdentityDto> relatedDataRepository)
public TimestampedValuesRepository(DbContext db)
{
this.db = db;
this.relatedDataRepository = relatedDataRepository;
}
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>()
.Include(e => e.ValuesIdentity);
protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
var timestampedValuesEntities = new List<TimestampedValues>();
foreach (var dto in dtos)
{
var keys = dto.Values.Keys.ToArray();
await CreateValuesIdentityIfNotExist(discriminatorId, keys, token);
var timestampedValuesEntity = new TimestampedValues()
{
DiscriminatorId = discriminatorId,
@ -44,7 +37,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
public async virtual Task<IEnumerable<TimestampedValuesDto>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> Get(Guid discriminatorId, DateTimeOffset? timestampBegin, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(entity => entity.DiscriminatorId == discriminatorId);
@ -59,68 +52,79 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
.OrderBy(item => item.Timestamp)
.Skip(skip)
.Take(take);
var data = await Materialize(discriminatorId, query, token);
var entities = await query.ToArrayAsync(token);
// Фильтрация по запрашиваемым полям
if (!columnNames.IsNullOrEmpty())
{
data = ReduceSetColumnsByNames(data, columnNames!);
}
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return data;
return result;
}
public async virtual Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var dtos = await Materialize(discriminatorId, query, token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return dtos;
return result;
}
public async virtual Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var dtos = await Materialize(discriminatorId, query, token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return dtos;
return result;
}
public async virtual Task<IEnumerable<TimestampedValuesDto>> GetResampledData(
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default)
{
var dtos = await GetGtDate(discriminatorId, dateBegin, token);
var result = await GetGtDate(discriminatorId, dateBegin, token);
var dateEnd = dateBegin.AddSeconds(intervalSec);
dtos = dtos
.Where(i => i.Timestamp <= dateEnd);
result = result
.Where(i => i.Item1 <= dateEnd);
var ratio = dtos.Count() / approxPointsCount;
var ratio = result.Count() / approxPointsCount;
if (ratio > 1)
dtos = dtos
result = result
.Where((_, index) => index % ratio == 0);
return dtos;
return result;
}
public async virtual Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
public async virtual Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.Timestamp > timestampBegin);
var entities = await query.ToArrayAsync(token);
var dtos = await Materialize(discriminatorId, query, token);
var result = entities.Select(e => Tuple.Create(
e.Timestamp,
e.Values
));
return dtos;
return result;
}
public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
@ -156,37 +160,12 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return query.CountAsync(token);
}
private async Task<IEnumerable<TimestampedValuesDto>> Materialize(Guid discriminatorId, IQueryable<TimestampedValues> query, CancellationToken token)
{
var valuesIdentities = await relatedDataRepository.Get(token);
var valuesIdentity = valuesIdentities?
.FirstOrDefault(e => e.DiscriminatorId == discriminatorId);
if (valuesIdentity == null)
return [];
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(entity =>
{
var dto = new TimestampedValuesDto()
{
Timestamp = entity.Timestamp.ToUniversalTime()
};
for (var i = 0; i < valuesIdentity.Identity.Count(); i++)
{
var key = valuesIdentity.Identity[i];
var value = entity.Values[i];
dto.Values.Add(key, value);
}
return dto;
});
return dtos;
}
/// <summary>
/// Применить фильтр по дате
/// </summary>
/// <param name="query"></param>
/// <param name="timestampBegin"></param>
/// <returns></returns>
private IQueryable<TimestampedValues> ApplyGeTimestamp(IQueryable<TimestampedValues> query, DateTimeOffset timestampBegin)
{
var geTimestampUtc = timestampBegin.ToUniversalTime();
@ -196,46 +175,4 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result;
}
private IEnumerable<TimestampedValuesDto> ReduceSetColumnsByNames(IEnumerable<TimestampedValuesDto> dtos, IEnumerable<string> columnNames)
{
var result = dtos.Select(dto =>
{
var reducedValues = dto.Values
.Where(v => columnNames.Contains(v.Key))
.ToDictionary();
dto.Values = reducedValues;
return dto;
});
return result;
}
private async Task CreateValuesIdentityIfNotExist(Guid discriminatorId, string[] keys, CancellationToken token)
{
var valuesIdentities = await relatedDataRepository.Get(token);
var valuesIdentity = valuesIdentities?
.FirstOrDefault(e => e.DiscriminatorId == discriminatorId);
if (valuesIdentity is null)
{
valuesIdentity = new ValuesIdentityDto()
{
DiscriminatorId = discriminatorId,
Identity = keys
};
await relatedDataRepository.Add(valuesIdentity, token);
return;
}
if (!valuesIdentity.Identity.SequenceEqual(keys))
{
var expectedIdentity = string.Join(", ", valuesIdentity.Identity);
var actualIdentity = string.Join(", ", keys);
throw new InvalidOperationException($"Для системы {discriminatorId.ToString()} " +
$"характерен набор данных: [{expectedIdentity}], однако был передан набор: [{actualIdentity}]");
}
}
}

View File

@ -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<DataSchemeDto?> GetByDiscriminator(Guid discriminatorId, CancellationToken token)
{
var result = memoryCache.Get<DataSchemeDto>(discriminatorId)
?? await base.GetByDiscriminator(discriminatorId, token);
return result;
}
}

View File

@ -1,27 +1,27 @@
using DD.Persistence.Repository.Repositories;
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.RepositoriesCached;
public class RelatedDataCachedRepository<TDto, TEntity> : RelatedDataRepository<TDto, TEntity>
where TEntity : class, new()
where TDto : class, new()
public class DataSourceSystemCachedRepository : DataSourceSystemRepository
{
private static readonly string SystemCacheKey = $"{typeof(TEntity).FullName}CacheKey";
private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey";
private readonly IMemoryCache memoryCache;
private readonly TimeSpan? AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60);
public RelatedDataCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
public DataSourceSystemCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{
this.memoryCache = memoryCache;
}
public override async Task Add(TDto dataSourceSystemDto, CancellationToken token)
public override async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token)
{
await base.Add(dataSourceSystemDto, token);
memoryCache.Remove(SystemCacheKey);
}
public override async Task<IEnumerable<TDto>> Get(CancellationToken token)
public override async Task<IEnumerable<DataSourceSystemDto>> Get(CancellationToken token)
{
var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async (cacheEntry) =>
{

View File

@ -12,7 +12,7 @@
// private const int CacheItemsCount = 3600;
// public TimestampedValuesCachedRepository(DbContext db, IRelatedDataRepository<ValuesIdentityDto> relatedDataRepository) : base(db, relatedDataRepository)
// public TimestampedValuesCachedRepository(DbContext db, IDataSourceSystemRepository<ValuesIdentityDto> relatedDataRepository) : base(db, relatedDataRepository)
// {
// //Task.Run(async () =>
// //{

View File

@ -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<Type, Dictionary<string, TypeAccessor>> TypePropSelectors { get; set; } = new();
private static MethodInfo GetExtOrderMethod(string methodName)
=> typeof(System.Linq.Queryable)
=> typeof(Queryable)
.GetMethods()
.Where(m => m.Name == methodName &&
m.IsGenericMethodDefinition &&

View File

@ -1,4 +1,4 @@
namespace DD.Persistence.Repository.Extensions;
namespace DD.Persistence.Extensions;
public static class IEnumerableExtensions
{

View File

@ -0,0 +1,25 @@
using DD.Persistence.Models;
namespace DD.Persistence.Repositories;
/// <summary>
/// Репозиторий для работы со схемами наборов данных
/// </summary>
public interface IDataSchemeRepository
{
/// <summary>
/// Добавить схему
/// </summary>
/// <param name="dataSourceSystemDto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token);
/// <summary>
/// Вычитать схему
/// </summary>
/// <param name="discriminatorId">Дискриминатор системы</param>
/// <param name="token"></param>
/// <returns></returns>
Task<DataSchemeDto?> GetByDiscriminator(Guid discriminatorId, CancellationToken token);
}

View File

@ -0,0 +1,23 @@
using DD.Persistence.Models;
namespace DD.Persistence.Repositories;
/// <summary>
/// Репозиторий для работы с системами - источниками данных
/// </summary>
public interface IDataSourceSystemRepository
{
/// <summary>
/// Добавить систему - источник данных
/// </summary>
/// <param name="dataSourceSystemDto"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token);
/// <summary>
/// Получить список систем - источников данных
/// </summary>
/// <returns></returns>
public Task<IEnumerable<DataSourceSystemDto>> Get(CancellationToken token);
}

View File

@ -1,23 +0,0 @@
namespace DD.Persistence.Repositories;
/// <summary>
/// Интерфейс по работе с простой структурой данных, подразумевающей наличие связи с более сложной
/// В контексте TechMessagesRepository это системы - источники данных
/// В контексте TimestampedValuesRepository это идентификационные наборы (ключи для значений в соответствии с индексами в хранимых массивах)
/// </summary>
public interface IRelatedDataRepository<TDto>
{
/// <summary>
/// Добавить данные
/// </summary>
/// <param name="dataSourceSystemDto"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task Add(TDto dataSourceSystemDto, CancellationToken token);
/// <summary>
/// Получить список данных
/// </summary>
/// <returns></returns>
public Task<IEnumerable<TDto>> Get(CancellationToken token);
}

View File

@ -4,14 +4,14 @@ using DD.Persistence.RepositoriesAbstractions;
namespace DD.Persistence.Repositories;
/// <summary>
/// Интерфейс по работе с временными данными
/// Репозиторий для работы с временными данными
/// </summary>
public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBaseRepository
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
@ -35,7 +35,7 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBase
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получение данных с начала
@ -44,7 +44,7 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBase
/// <param name="takeCount">Количество</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token);
Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token);
/// <summary>
/// Получение данных с конца
@ -53,5 +53,5 @@ public interface ITimestampedValuesRepository : ISyncRepository, ITimeSeriesBase
/// <param name="takeCount">Количество</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token);
Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token);
}

View File

@ -6,7 +6,7 @@ namespace DD.Persistence.RepositoriesAbstractions;
/// <summary>
/// Интерфейс по работе с данными
/// </summary>
public interface ISyncRepository
public interface ISyncRepository // ToDo: исчерпывающая абстракция
{
/// <summary>
/// Получить данные, начиная с определенной даты
@ -15,7 +15,7 @@ public interface ISyncRepository
/// <param name="dateBegin">дата начала</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token);
Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token);
/// <summary>

View File

@ -5,7 +5,7 @@ namespace DD.Persistence.RepositoriesAbstractions;
/// <summary>
/// Интерфейс по работе с прореженными данными
/// </summary>
public interface ITimeSeriesBaseRepository
public interface ITimeSeriesBaseRepository // ToDo: исчерпывающая абстракция
{
/// <summary>
/// Получить список объектов с прореживанием
@ -16,7 +16,7 @@ public interface ITimeSeriesBaseRepository
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetResampledData(
Task<IEnumerable<Tuple<DateTimeOffset, object[]>>> GetResampledData(
Guid discriminatorId,
DateTimeOffset dateBegin,
double intervalSec = 600d,

View File

@ -0,0 +1,85 @@
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
namespace DD.Persistence.Services.Interfaces;
/// <summary>
/// Сервис для работы с временными данными
/// </summary>
public interface ITimestampedValuesService
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token);
/// <summary>
/// Количество записей по указанному набору в БД. Для пагинации
/// </summary>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Count(Guid discriminatorId, CancellationToken token);
/// <summary>
/// Получение данных с фильтрацией. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="geTimestamp">Фильтр позднее даты</param>
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получение данных с начала
/// </summary>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="takeCount">Количество</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token);
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="timestampBegin">дата начала</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token);
/// <summary>
/// Получение данных с конца
/// </summary>
/// <param name="discriminatorId">Дискриминатор (идентификатор) набора</param>
/// <param name="takeCount">Количество</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token);
/// <summary>
/// Получить список объектов с прореживанием
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="timestampBegin">дата начала</param>
/// <param name="intervalSec"></param>
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TimestampedValuesDto>> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default);
/// <summary>
/// Получить диапазон дат, для которых есть данные
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token);
}

View File

@ -0,0 +1,201 @@
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;
/// <inheritdoc/>
public class TimestampedValuesService : ITimestampedValuesService
{
private readonly ITimestampedValuesRepository timestampedValuesRepository;
private readonly IDataSchemeRepository dataSchemeRepository;
/// <inheritdoc/>
public TimestampedValuesService(ITimestampedValuesRepository timestampedValuesRepository, IDataSchemeRepository relatedDataRepository)
{
this.timestampedValuesRepository = timestampedValuesRepository;
this.dataSchemeRepository = relatedDataRepository;
}
/// <inheritdoc/>
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{
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;
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? 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;
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{
var result = await timestampedValuesRepository.GetFirst(discriminatorId, takeCount, token);
var dtos = await Materialize(discriminatorId, result, token);
return dtos;
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{
var result = await timestampedValuesRepository.GetLast(discriminatorId, takeCount, token);
var dtos = await Materialize(discriminatorId, result, token);
return dtos;
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> 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;
}
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset beginTimestamp, CancellationToken token)
{
var result = await timestampedValuesRepository.GetGtDate(discriminatorId, beginTimestamp, token);
var dtos = await Materialize(discriminatorId, result, token);
return dtos;
}
/// <inheritdoc/>
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
{
var result = await timestampedValuesRepository.Count(discriminatorId, token);
return result;
}
/// <inheritdoc/>
public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var result = await timestampedValuesRepository.GetDatesRange(discriminatorId, token);
return result;
}
/// <summary>
/// Преобразовать результат запроса в набор dto
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="queryResult"></param>
/// <param name="token"></param>
/// <returns></returns>
private async Task<IEnumerable<TimestampedValuesDto>> Materialize(Guid discriminatorId, IEnumerable<Tuple<DateTimeOffset, object[]>> queryResult, CancellationToken token)
{
var systemSpecification = await dataSchemeRepository.GetByDiscriminator(discriminatorId, token);
if (systemSpecification is null)
{
return [];
}
var dtos = queryResult.Select(entity =>
{
var dto = new TimestampedValuesDto()
{
Timestamp = entity.Item1.ToUniversalTime()
};
var identity = systemSpecification!.PropNames;
for (var i = 0; i < identity.Count(); i++)
{
var key = identity[i];
var value = entity.Item2[i];
dto.Values.Add(key, value);
}
return dto;
});
return dtos;
}
/// <summary>
/// Создать спецификацию, при отсутствии таковой
/// </summary>
/// <param name="discriminatorId">Дискриминатор системы</param>
/// <param name="fieldNames">Набор наименований полей</param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Некорректный набор наименований полей</exception>
private async Task CreateSystemSpecificationIfNotExist(Guid discriminatorId, string[] fieldNames, CancellationToken token)
{
var systemSpecification = await dataSchemeRepository.GetByDiscriminator(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}]");
}
}
/// <summary>
/// Отсеить лишние поля в соответствии с заданным фильтром
/// </summary>
/// <param name="dtos"></param>
/// <param name="fieldNames">Поля, которые необходимо оставить</param>
/// <returns></returns>
private IEnumerable<TimestampedValuesDto> ReduceSetColumnsByNames(IEnumerable<TimestampedValuesDto> dtos, IEnumerable<string> fieldNames)
{
var result = dtos.Select(dto =>
{
var reducedValues = dto.Values
.Where(v => fieldNames.Contains(v.Key))
.ToDictionary();
dto.Values = reducedValues;
return dto;
});
return result;
}
}