#408 Репозиторий хранения данных WITS0 #7

Merged
on.nemtina merged 9 commits from WitsData into master 2024-12-12 16:11:56 +05:00
34 changed files with 14146 additions and 70 deletions

View File

@ -156,7 +156,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
public async Task<ActionResult<IEnumerable<DataWithWellDepthAndSectionDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
@ -166,7 +166,7 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
[HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesRange(idDiscriminator, token);

View File

@ -0,0 +1,86 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Services.Interfaces;
namespace Persistence.API.Controllers;
/// <summary>
/// Работа с параметрами Wits
/// </summary>
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class WitsDataController : ControllerBase, IWitsDataApi
{
private readonly IWitsDataService witsDataService;
public WitsDataController(IWitsDataService witsDataService)
{
this.witsDataService = witsDataService;
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{discriminatorId}/datesRange")]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid discriminatorId, CancellationToken token)

discriminatorId лучше добавить в роут

discriminatorId лучше добавить в роут
{
var result = await witsDataService.GetDatesRangeAsync(discriminatorId, token);
return result == null ? NoContent() : Ok(result);
}
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{discriminatorId}/part")]
public async Task<ActionResult<IEnumerable<WitsDataDto>>> GetPart([FromRoute] Guid discriminatorId, [FromQuery] DateTimeOffset dateBegin, [FromQuery] int take, CancellationToken token)

discriminatorId лучше добавить в роут

discriminatorId лучше добавить в роут
{
var result = await witsDataService.GetPart(discriminatorId, dateBegin, take, token);
return Ok(result);
}
/// <summary>
/// Получить набор параметров (Wits) для построения графика
/// </summary>
/// <param name="discriminatorId">Дискриминатор системы</param>
/// <param name="dateFrom">Начало временного интервала</param>
/// <param name="dateTo">Конец временного интервала</param>
/// <param name="approxPointsCount">Количество точек</param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{discriminatorId}/graph")]
public async Task<ActionResult<IEnumerable<WitsDataDto>>> GetValuesForGraph([FromRoute] Guid discriminatorId,

discriminatorId лучше добавить в роут

discriminatorId лучше добавить в роут
[FromQuery] DateTimeOffset dateFrom, [FromQuery] DateTimeOffset dateTo, [FromQuery] int approxPointsCount, CancellationToken token)
{
var result = await witsDataService.GetValuesForGraph(discriminatorId, dateFrom, dateTo, approxPointsCount, token);
return Ok(result);
}
/// <summary>
/// Сохранить набор параметров (Wits)
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange([FromBody] IEnumerable<WitsDataDto> dtos, CancellationToken token)
{
var result = await witsDataService.AddRange(dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
}

View File

@ -6,6 +6,8 @@ using Microsoft.OpenApi.Models;
using Persistence.Database.Entity;
using Persistence.Models;
using Persistence.Models.Configurations;
using Persistence.Services;
using Persistence.Services.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
@ -55,8 +57,13 @@ public static class DependencyInjection
});
}
#region Authentication
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
public static void AddServices(this IServiceCollection services)
{
services.AddTransient<IWitsDataService, WitsDataService>();
}
#region Authentication
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak")

View File

@ -25,13 +25,13 @@ public class Startup
services.AddAuthorization();
services.AddJWTAuthentication(Configuration);
services.AddMemoryCache();
services.AddServices();
DependencyInjection.MapsterSetup();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI();

View File

@ -30,7 +30,7 @@ public interface IChangeLogClient
/// <returns></returns>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<DataWithWellDepthAndSectionDto>>> GetByDate(
Guid idDiscriminator,
Guid idDiscriminator,
DateTimeOffset moment,
[Query] SectionPartRequest filterRequest,
[Query] PaginationRequest paginationRequest);

View File

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Refit;
namespace Persistence.Client.Clients;
public interface IWitsDataClient
{
private const string BaseRoute = "/api/witsData";
[Get($"{BaseRoute}/{{discriminatorId}}/graph")]

discriminatorId лучше добавить в роут

discriminatorId лучше добавить в роут
Task<IApiResponse<IEnumerable<WitsDataDto>>> GetValuesForGraph(Guid discriminatorId, [Query] DateTimeOffset dateFrom, [Query] DateTimeOffset dateTo, [Query] int approxPointsCount, CancellationToken token);
[Post($"{BaseRoute}/")]
Task<IApiResponse<int>> AddRange(IEnumerable<WitsDataDto> dtos, CancellationToken token);
[Get($"{BaseRoute}/{{discriminatorId}}/part")]
Task<IApiResponse<IEnumerable<WitsDataDto>>> GetPart(Guid discriminatorId, [Query] DateTimeOffset dateBegin, [Query] int take = 24 * 60 * 60, CancellationToken token = default);
[Get($"{BaseRoute}/{{discriminatorId}}/datesRange")]
Task<IApiResponse<DatesRangeDto>> GetDatesRangeAsync(Guid discriminatorId, CancellationToken token);
}

View File

@ -0,0 +1,257 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Persistence.Database.Model;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistenceDbContext))]
[Migration("20241203120141_ParameterDataMigration")]
partial class ParameterDataMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseCollation("Russian_Russia.1251")
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Persistence.Database.Entity.DrillingSystem", b =>
{
b.Property<Guid>("SystemId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id системы автобурения");
b.Property<string>("Description")
.HasColumnType("text")
.HasComment("Описание системы автобурения");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(256)")
.HasComment("Наименование системы автобурения");
b.HasKey("SystemId");
b.ToTable("DrillingSystem");
});
modelBuilder.Entity("Persistence.Database.Entity.ParameterData", b =>
{
b.Property<int>("DiscriminatorId")
.HasColumnType("integer")
.HasComment("Дискриминатор системы");
b.Property<int>("ParameterId")
.HasColumnType("integer")
.HasComment("Id параметра");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Временная отметка");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("varchar(256)")
.HasComment("Значение параметра в виде строки");
b.HasKey("DiscriminatorId", "ParameterId", "Timestamp");
b.ToTable("ParameterData");
});
modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b =>
{
b.Property<Guid>("EventId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("Id события");
b.Property<int>("CategoryId")
.HasColumnType("integer")
.HasComment("Id Категории важности");
b.Property<double?>("Depth")
.HasColumnType("double precision")
.HasComment("Глубина забоя");
b.Property<string>("MessageText")
.IsRequired()
.HasColumnType("varchar(512)")
.HasComment("Текст сообщения");
b.Property<Guid>("SystemId")
.HasColumnType("uuid")
.HasComment("Id системы автобурения, к которой относится сообщение");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Дата возникновения");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasComment("Id пользователя за пультом бурильщика");
b.HasKey("EventId");
b.HasIndex("SystemId");
b.ToTable("TechMessage");
});
modelBuilder.Entity("Persistence.Database.Entity.TimestampedSet", b =>
{
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор ссылка на тип сохраняемых данных");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Отметка времени, строго в UTC");
b.Property<string>("Set")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Набор сохраняемых данных");
b.HasKey("IdDiscriminator", "Timestamp");
b.ToTable("TimestampedSets", t =>
{
t.HasComment("Общая таблица данных временных рядов");
});
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{
b.Property<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");
b.Property<double?>("AxialLoad")
.HasColumnType("double precision")
.HasColumnName("axialLoad");
b.Property<double?>("BitDepth")
.HasColumnType("double precision")
.HasColumnName("bitDepth");
b.Property<double?>("BlockPosition")
.HasColumnType("double precision")
.HasColumnName("blockPosition");
b.Property<double?>("BlockSpeed")
.HasColumnType("double precision")
.HasColumnName("blockSpeed");
b.Property<double?>("Flow")
.HasColumnType("double precision")
.HasColumnName("flow");
b.Property<double?>("HookWeight")
.HasColumnType("double precision")
.HasColumnName("hookWeight");
b.Property<int>("IdFeedRegulator")
.HasColumnType("integer")
.HasColumnName("idFeedRegulator");
b.Property<int?>("Mode")
.HasColumnType("integer")
.HasColumnName("mode");
b.Property<double?>("Mse")
.HasColumnType("double precision")
.HasColumnName("mse");
b.Property<short>("MseState")
.HasColumnType("smallint")
.HasColumnName("mseState");
b.Property<double?>("Pressure")
.HasColumnType("double precision")
.HasColumnName("pressure");
b.Property<double?>("Pump0Flow")
.HasColumnType("double precision")
.HasColumnName("pump0Flow");
b.Property<double?>("Pump1Flow")
.HasColumnType("double precision")
.HasColumnName("pump1Flow");
b.Property<double?>("Pump2Flow")
.HasColumnType("double precision")
.HasColumnName("pump2Flow");
b.Property<double?>("RotorSpeed")
.HasColumnType("double precision")
.HasColumnName("rotorSpeed");
b.Property<double?>("RotorTorque")
.HasColumnType("double precision")
.HasColumnName("rotorTorque");
b.Property<string>("User")
.HasColumnType("text")
.HasColumnName("user");
b.Property<double?>("WellDepth")
.HasColumnType("double precision")
.HasColumnName("wellDepth");
b.HasKey("Date");
b.ToTable("DataSaub");
});
modelBuilder.Entity("Persistence.Database.Model.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания уставки");
b.Property<Guid>("IdUser")
.HasColumnType("uuid")
.HasComment("Id автора последнего изменения");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Created");
b.ToTable("Setpoint");
});
modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b =>
{
b.HasOne("Persistence.Database.Entity.DrillingSystem", "System")
.WithMany()
.HasForeignKey("SystemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("System");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class ParameterDataMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ParameterData",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор системы"),
ParameterId = table.Column<int>(type: "integer", nullable: false, comment: "Id параметра"),
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Временная отметка"),
Value = table.Column<string>(type: "varchar(256)", nullable: false, comment: "Значение параметра в виде строки")
},
constraints: table =>
{
table.PrimaryKey("PK_ParameterData", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ParameterData");
}
}
}

View File

@ -46,6 +46,30 @@ namespace Persistence.Database.Postgres.Migrations
b.ToTable("DrillingSystem");
});
modelBuilder.Entity("Persistence.Database.Entity.ParameterData", b =>
{
b.Property<int>("DiscriminatorId")
.HasColumnType("integer")
.HasComment("Дискриминатор системы");
b.Property<int>("ParameterId")
.HasColumnType("integer")
.HasComment("Id параметра");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Временная отметка");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("varchar(256)")
.HasComment("Значение параметра в виде строки");
b.HasKey("DiscriminatorId", "ParameterId", "Timestamp");
b.ToTable("ParameterData");
});
modelBuilder.Entity("Persistence.Database.Entity.TechMessage", b =>
{
b.Property<Guid>("EventId")

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Persistence.Database.Entity;
[PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))]
public class ParameterData
{
[Required, Comment("Дискриминатор системы")]
public Guid DiscriminatorId { get; set; }
[Comment("Id параметра")]
public int ParameterId { get; set; }
[Column(TypeName = "varchar(256)"), Comment("Значение параметра в виде строки")]
public required string Value { get; set; }
[Comment("Временная отметка")]
public DateTimeOffset Timestamp { get; set; }
}

View File

@ -17,7 +17,11 @@ public class PersistenceDbContext : DbContext
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public PersistenceDbContext(DbContextOptions options)
public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>();
public PersistenceDbContext(DbContextOptions options)
: base(options)
{

View File

@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Entity;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
@ -26,8 +27,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task ClearAndInsertRange_InEmptyDb()
{
// arrange
var idDiscriminator = Guid.NewGuid();
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2, DateTimeOffset.UtcNow);
// act
@ -91,8 +94,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task Update_returns_success()
{
// arrange
var idDiscriminator = Guid.NewGuid();
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto);
@ -232,9 +237,11 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact]
public async Task GetByDate_returns_success()
{
// arrange
//создаем записи
var count = 5;
// arrange
dbContext.CleanupDbSet<ChangeLog>();
//создаем записи
var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
@ -284,9 +291,11 @@ public class ChangeLogControllerTest : BaseIntegrationTest
int daysAfterNowFilter,
int changeLogCount)
{
// arrange
//создаем записи
var count = insertedCount;
// arrange
dbContext.CleanupDbSet<ChangeLog>();
//создаем записи
var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
@ -333,7 +342,7 @@ public class ChangeLogControllerTest : BaseIntegrationTest
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
Guid idDiscriminator = Guid.NewGuid();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d =>
{

View File

@ -140,7 +140,7 @@ namespace Persistence.IntegrationTests.Controllers
dbContext.CleanupDbSet<Setpoint>();
//act
var response = await setpointClient.GetDatesRangeAsync(new CancellationToken());
var response = await setpointClient.GetDatesRangeAsync(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -159,10 +159,10 @@ namespace Persistence.IntegrationTests.Controllers
var dateBegin = DateTimeOffset.MinValue;
var take = 1;
var part = await setpointClient.GetPart(dateBegin, take, new CancellationToken());
var part = await setpointClient.GetPart(dateBegin, take, CancellationToken.None);
//act
var response = await setpointClient.GetDatesRangeAsync(new CancellationToken());
var response = await setpointClient.GetDatesRangeAsync(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -188,7 +188,7 @@ namespace Persistence.IntegrationTests.Controllers
var take = 2;
//act
var response = await setpointClient.GetPart(dateBegin, take, new CancellationToken());
var response = await setpointClient.GetPart(dateBegin, take, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -205,7 +205,7 @@ namespace Persistence.IntegrationTests.Controllers
await Add();
//act
var response = await setpointClient.GetPart(dateBegin, take, new CancellationToken());
var response = await setpointClient.GetPart(dateBegin, take, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

View File

@ -1,4 +1,4 @@
using System.Net;
using System.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
@ -33,7 +33,7 @@ namespace Persistence.IntegrationTests.Controllers
dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
var PaginationRequest = new PaginationRequest()
var requestDto = new PaginationRequest()
{
Skip = 1,
Take = 2,
@ -41,14 +41,14 @@ namespace Persistence.IntegrationTests.Controllers
};
//act
var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken());
var response = await techMessagesClient.GetPage(requestDto, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content.Items);
Assert.Equal(PaginationRequest.Skip, response.Content.Skip);
Assert.Equal(PaginationRequest.Take, response.Content.Take);
Assert.Equal(requestDto.Skip, response.Content.Skip);
Assert.Equal(requestDto.Take, response.Content.Take);
}
[Fact]
@ -57,7 +57,7 @@ namespace Persistence.IntegrationTests.Controllers
//arrange
var dtos = await InsertRange();
var dtosCount = dtos.Count();
var PaginationRequest = new PaginationRequest()
var requestDto = new PaginationRequest()
{
Skip = 0,
Take = 2,
@ -65,7 +65,7 @@ namespace Persistence.IntegrationTests.Controllers
};
//act
var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken());
var response = await techMessagesClient.GetPage(requestDto, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -98,7 +98,7 @@ namespace Persistence.IntegrationTests.Controllers
};
//act
var response = await techMessagesClient.AddRange(dtos, new CancellationToken());
var response = await techMessagesClient.AddRange(dtos, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
@ -113,7 +113,7 @@ namespace Persistence.IntegrationTests.Controllers
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
//act
var response = await techMessagesClient.GetSystems(new CancellationToken());
var response = await techMessagesClient.GetSystems(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -132,7 +132,7 @@ namespace Persistence.IntegrationTests.Controllers
.ToArray();
//act
var response = await techMessagesClient.GetSystems(new CancellationToken());
var response = await techMessagesClient.GetSystems(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -153,7 +153,7 @@ namespace Persistence.IntegrationTests.Controllers
var autoDrillingSystem = nameof(TechMessageDto.System);
//act
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken());
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -171,7 +171,7 @@ namespace Persistence.IntegrationTests.Controllers
var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem);
//act
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken());
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -186,7 +186,7 @@ namespace Persistence.IntegrationTests.Controllers
public async Task GetDatesRange_returns_success()
{
//act
var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken());
var response = await techMessagesClient.GetDatesRangeAsync(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -202,7 +202,7 @@ namespace Persistence.IntegrationTests.Controllers
await InsertRange();
//act
var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken());
var response = await techMessagesClient.GetDatesRangeAsync(CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -219,7 +219,7 @@ namespace Persistence.IntegrationTests.Controllers
var take = 2;
//act
var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken());
var response = await techMessagesClient.GetPart(dateBegin, take, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -236,7 +236,7 @@ namespace Persistence.IntegrationTests.Controllers
await InsertRange();
//act
var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken());
var response = await techMessagesClient.GetPart(dateBegin, take, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -277,7 +277,7 @@ namespace Persistence.IntegrationTests.Controllers
//act
var response = await techMessagesClient.AddRange(dtos, new CancellationToken());
var response = await techMessagesClient.AddRange(dtos, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);

View File

@ -0,0 +1,235 @@
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Entity;
using Persistence.Models;
using Xunit;
namespace Persistence.IntegrationTests.Controllers;
public class WitsDataControllerTest : BaseIntegrationTest
{
private IWitsDataClient witsDataClient;
public WitsDataControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var scope = factory.Services.CreateScope();
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
witsDataClient = persistenceClientFactory.GetClient<IWitsDataClient>();
}
[Fact]
public async Task GetDatesRangeAsync_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var discriminatorId = Guid.NewGuid();
//act
var response = await witsDataClient.GetDatesRangeAsync(discriminatorId, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
}
[Fact]
public async Task GetPart_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var discriminatorId = Guid.NewGuid();
var dateBegin = DateTimeOffset.UtcNow;
var take = 1;
//act
var response = await witsDataClient.GetPart(discriminatorId, dateBegin, take, CancellationToken.None);

Лучше в тестах писать не new CancellationToken(), а CancellationToken.None (по согласованию с Никитой)

Лучше в тестах писать не new CancellationToken(), а CancellationToken.None (по согласованию с Никитой)
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task InsertRange_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
//act
await AddRange();
}
[Fact]
public async Task GetValuesForGraph_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var discriminatorId = Guid.NewGuid();
var dateFrom = DateTimeOffset.UtcNow;
var dateTo = DateTimeOffset.UtcNow;
var approxPointCount = 12;
//act
var response = await witsDataClient.GetValuesForGraph(discriminatorId, dateFrom, dateTo, approxPointCount, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetDatesRangeAsync_AfterSave_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var dtos = await AddRange();
var discriminatorId = dtos.FirstOrDefault()!.DiscriminatorId;
//act
var response = await witsDataClient.GetDatesRangeAsync(discriminatorId, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var expectedDateFrom = dtos
.Select(e => e.Timestamped)
.Min()
.ToString("dd.MM.yyyy-HH:mm:ss");
var actualDateFrom = response.Content.From.DateTime
.ToString("dd.MM.yyyy-HH:mm:ss");
Assert.Equal(expectedDateFrom, actualDateFrom);
var expectedDateTo = dtos
Review

Здесь 2 раза сравнивается минимум с From

Здесь 2 раза сравнивается минимум с From
.Select(e => e.Timestamped)
.Max()
.ToString("dd.MM.yyyy-HH:mm:ss");
var actualDateTo = response.Content.To.DateTime
.ToString("dd.MM.yyyy-HH:mm:ss");
Assert.Equal(expectedDateTo, actualDateTo);
}
[Fact]
public async Task GetPart_AfterSave_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var dtos = await AddRange();
var discriminatorId = dtos.FirstOrDefault()!.DiscriminatorId;
var dateBegin = dtos.FirstOrDefault()!.Timestamped;
var take = 1;
//act
var response = await witsDataClient.GetPart(discriminatorId, dateBegin, take, CancellationToken.None);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(take, response.Content.Count());
Review

Здесь желательно проверить не только количество элементов, но и содержание (id, discriminatorId или что-то в этом роде): вдруг вернулся не тот элемент?

Здесь желательно проверить не только количество элементов, но и содержание (id, discriminatorId или что-то в этом роде): вдруг вернулся не тот элемент?
var expectedDto = dtos.FirstOrDefault();
var actualDto = response.Content.FirstOrDefault();
Assert.Equal(expectedDto?.DiscriminatorId, actualDto?.DiscriminatorId);
var expectedValueDto = expectedDto?.Values.FirstOrDefault();
var actualValueDto = actualDto?.Values.FirstOrDefault();
Assert.Equal(expectedValueDto?.ItemId, actualValueDto?.ItemId);
Assert.Equal(expectedValueDto?.RecordId, actualValueDto?.RecordId);
}
[Fact]
public async Task GetValuesForGraph_AfterSave_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var dtos = await AddRange(37);
var discriminatorId = dtos.FirstOrDefault()!.DiscriminatorId;
var dateFrom = dtos.Select(e => e.Timestamped).Min();
var dateTo = dtos.Select(e => e.Timestamped).Max();
var approxPointCount = 12;
//act
var response = await witsDataClient.GetValuesForGraph(discriminatorId, dateFrom, dateTo, approxPointCount, CancellationToken.None);

AddRange_returns_BadRequest

AddRange_returns_BadRequest
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Equal(approxPointCount, response.Content.Count());
}
[Fact]
public async Task AddRange_returns_BadRequest()
{
//arrange
var dtos = new List<WitsDataDto>()
{
new WitsDataDto()
{
DiscriminatorId = Guid.NewGuid(),
Timestamped = DateTimeOffset.UtcNow,
Values = new List<WitsValueDto>()
{
new WitsValueDto()
{
RecordId = -1, // < 0
ItemId = 101, // > 100
Value = string.Empty
}
}
}
};
//act
var response = await witsDataClient.AddRange(dtos, CancellationToken.None);

i < countToCreate && i < 100
А это так и нужно?

i < countToCreate && i < 100 А это так и нужно?

По сути нет, это на всякий случай - вдруг кому то когда нибудь придёт идея написать в этом контроллере тест и передать в качестве count >100. Хотя, наверное, лучше тогда человеку сразу увидеть ошибку, чем гадать - почему он говорит создать 150 записей, а создаётся 100 ))

По сути нет, это на всякий случай - вдруг кому то когда нибудь придёт идея написать в этом контроллере тест и передать в качестве count >100. Хотя, наверное, лучше тогда человеку сразу увидеть ошибку, чем гадать - почему он говорит создать 150 записей, а создаётся 100 ))
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private async Task<IEnumerable<WitsDataDto>> AddRange(int countToCreate = 10)
{
var dtos = new List<WitsDataDto>();
var discriminatorId = Guid.NewGuid();
var timestamped = DateTimeOffset.UtcNow;
for (var i = 0; i < countToCreate; i++)
{
var random = new Random();

Лучше new Random() вынести на уровень выше, чтобы в цикле не создавался каждый раз новый экземпляр

Лучше new Random() вынести на уровень выше, чтобы в цикле не создавался каждый раз новый экземпляр
dtos.Add(new WitsDataDto()
{
DiscriminatorId = discriminatorId,
Timestamped = timestamped.AddSeconds(i),
Values = new List<WitsValueDto>()
{
new WitsValueDto()
{
RecordId = i + 1,
ItemId = i + 1,
Value = random.Next(1, 100)
}
}
});
}
//act
var response = await witsDataClient.AddRange(dtos, CancellationToken.None);
//assert
var count = dtos.SelectMany(e => e.Values).Count();
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal(count, response.Content);
return dtos;
}
}

View File

@ -34,6 +34,7 @@ public static class DependencyInjection
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
return services;
}

View File

@ -127,7 +127,7 @@ public class ChangeLogRepository : IChangeLogRepository
}
public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(
Guid idDiscriminator,
Guid idDiscriminator,
DateTimeOffset momentUtc,
SectionPartRequest filterRequest,
PaginationRequest paginationRequest,

View File

@ -0,0 +1,87 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Persistence.Database.Entity;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.Repository.Repositories;
public class ParameterRepository : IParameterRepository
{
private DbContext db;
public ParameterRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == idDiscriminator)
.GroupBy(e => 1)
.Select(group => new
{
Min = group.Min(e => e.Timestamp),
Max = group.Max(e => e.Timestamp),
});
var values = await query.FirstOrDefaultAsync(token);
Review

Тут values может быть null. И если будет null, то null нужно и вернуть. Соответственно, в нужных методах контроллера также необходимо будет поправить возвращаемый тип: вместо Ok возвращать NoContent.

PS: У меня тоже есть такой же метод GetDatesRangeAsync и Никита писал к этому методу вот такие замечания

Тут values может быть null. И если будет null, то null нужно и вернуть. Соответственно, в нужных методах контроллера также необходимо будет поправить возвращаемый тип: вместо Ok возвращать NoContent. PS: У меня тоже есть такой же метод GetDatesRangeAsync и Никита писал к этому методу вот такие замечания
Review

Сделал так: image
Мы же договаривались только для Post методов использовать Task<IActionResult> и ProducesResponseType

Сделал так: ![image](/attachments/35d115c7-e986-4cff-9b21-22c9e8df6877) <br> Мы же договаривались только для Post методов использовать `Task<IActionResult>` и `ProducesResponseType`
var result = new DatesRangeDto()
{
From = values?.Min ?? DateTimeOffset.MinValue,
To = values?.Max ?? DateTimeOffset.MaxValue
};
return result;
}
public async Task<IEnumerable<ParameterDto>> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token)
{
var query = GetQueryReadOnly();
var universalDate = dateBegin.ToUniversalTime();
var entities = await query
.Where(e => e.DiscriminatorId == idDiscriminator && e.Timestamp >= universalDate)
.Take(take)
.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<ParameterDto>());
return dtos;
}
public async Task<IEnumerable<ParameterDto>> GetValuesForGraph(Guid discriminatorId, DateTimeOffset dateFrom, DateTimeOffset dateTo,
int approxPointsCount, int? ratio, CancellationToken token)
{
var query = db.Set<ParameterData>().AsNoTracking();
var universalDateFrom = dateFrom.ToUniversalTime();
var universalDateTo = dateTo.ToUniversalTime();
query = query
.Where(e => e.DiscriminatorId == discriminatorId)

Тут для удобства чтения лучше разделиnь Where на 2 части:
.Where(e => e.DiscriminatorId == discriminatorId)
.Where(e => e.Timestamp >= universalDateFrom && e.Timestamp <= universalDateTo)

Тут для удобства чтения лучше разделиnь Where на 2 части: .Where(e => e.DiscriminatorId == discriminatorId) .Where(e => e.Timestamp >= universalDateFrom && e.Timestamp <= universalDateTo)
.Where(e => e.Timestamp >= universalDateFrom && e.Timestamp <= universalDateTo)
.OrderBy(e => e.Timestamp);
if (ratio != null)
{
query = query.Where(e => ((int) (e.Timestamp - dateFrom).TotalSeconds) % ratio == 0);
}
var entities = await query
.Take((int)(2.5 * approxPointsCount))
Review

ToArrayAsync(token)

ToArrayAsync(token)
.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<ParameterDto>());
return dtos;
}
public async Task<int> AddRange(IEnumerable<ParameterDto> dtos, CancellationToken token)
{
var entities = dtos.Select(e => e.Adapt<ParameterData>());
await db.Set<ParameterData>().AddRangeAsync(entities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using System;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
@ -49,13 +50,13 @@ public interface IChangeLogApi : ISyncWithDiscriminatorApi<DataWithWellDepthAndS
/// <returns></returns>
Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>

View File

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
namespace Persistence.API;
@ -8,21 +8,21 @@ namespace Persistence.API;
/// </summary>
public interface ISyncWithDiscriminatorApi<TDto>
{
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take">количество записей</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default);
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take">количество записей</param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default);

default у CancellationToken лучше убрать

default у CancellationToken лучше убрать

Тогда и у take надо убирать
Иначе порядок придётся у них менять

Тогда и у take надо убирать Иначе порядок придётся у них менять
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token);
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token);
}

View File

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
namespace Persistence.API;
/// <summary>
/// Интерфейс для работы с параметрами Wits
/// </summary>
public interface IWitsDataApi : ISyncWithDiscriminatorApi<WitsDataDto>
{
/// <summary>
/// Получить набор параметров (Wits) для построения графика
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<IEnumerable<WitsDataDto>>> GetValuesForGraph(Guid discriminatorId, DateTimeOffset dateFrom, DateTimeOffset dateTo,
int approxPointsCount, CancellationToken token);
/// <summary>
/// Сохранить набор параметров (Wits)
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> AddRange(IEnumerable<WitsDataDto> dtos, CancellationToken token);
}

View File

@ -0,0 +1,15 @@
using Persistence.Models.Enumerations;
namespace Persistence.Models.Configurations;
/// <summary>

Тут нужны комментарии, без комментариев не очень понятно

Тут нужны комментарии, без комментариев не очень понятно
/// Протокол Wits
/// </summary>
public class WitsInfo
{
public int RecordId { get; set; }
public int ItemId { get; set; }
public WitsType ValueType { get; set; }
}

View File

@ -0,0 +1,24 @@
namespace Persistence.Models.Enumerations;
/// <summary>
/// WITS Data Type Codes
/// </summary>
public enum WitsType
{
/// <summary>
/// Alphanumeric, Rep Code = 65
/// </summary>
A,
/// <summary>
/// 32 bit 2's complement signed integer, Rep Code = 73
/// </summary>
L,
/// <summary>
/// 16 bit 2's complement signed integer, Rep Code = 79
/// </summary>
S,
/// <summary>
/// 32 bit IEEE single precision floating point, Rep Code = 128
/// </summary>
F,
}

View File

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace Persistence.Models;
/// <summary>
/// Модель параметра
/// </summary>
public class ParameterDto
{
/// <summary>
/// Дискриминатор системы
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Id параметра
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "Id параметра не может быть меньше 0")]
public int ParameterId { get; set; }
/// <summary>
/// Значение параметра в виде строки
/// </summary>
[StringLength(256, MinimumLength = 1, ErrorMessage = "Допустимая длина значения параметра от 1 до 256 символов")]
public required string Value { get; set; }
/// <summary>
/// Временная отметка
/// </summary>
public DateTimeOffset Timestamp { get; set; }
}

View File

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace Persistence.Models;
/// <summary>
/// Группа параметров Wits
/// </summary>
public class WitsDataDto
{
/// <summary>
/// Временная отметка
/// </summary>
public required DateTimeOffset Timestamped { get; set; }
/// <summary>
/// Дискриминатор системы
/// </summary>
public required Guid DiscriminatorId { get; set; }
/// <summary>
/// Параметры
/// </summary>
public IEnumerable<WitsValueDto> Values { get; set; } = [];
}

View File

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace Persistence.Models;
/// <summary>
/// Параметр Wits
/// </summary>
public class WitsValueDto
{
/// <summary>
/// Wits - Record Number
/// </summary>
[Range(1, 100, ErrorMessage = "Значение \"Record Number\" обязано находиться в диапозоне от 1 до 100")]
public int RecordId { get; set; }
/// <summary>
/// Wits - Record Item
/// </summary>
[Range(1, 100, ErrorMessage = "Значение \"Wits Record Item\" обязано находиться в диапозоне от 1 до 100")]
public int ItemId { get; set; }
/// <summary>
/// Значение параметра
/// </summary>
public required object Value { get; set; }
}

View File

@ -6,6 +6,14 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Services\Config\WitsConfig.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Services\Config\WitsConfig.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />

View File

@ -0,0 +1,43 @@
using Persistence.Models;
namespace Persistence.Repositories;
public interface IParameterRepository
{
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ParameterDto>> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получить набор параметров (Wits) для построения графика
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <param name="approxPointsCount"></param>
/// <param name="ratio"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ParameterDto>> GetValuesForGraph(Guid discriminatorId, DateTimeOffset dateFrom, DateTimeOffset dateTo,
int approxPointsCount, int? ratio, CancellationToken token);
/// <summary>
/// Сохранить набор параметров (Wits)
/// </summary>
/// <param name="dtos"></param>
/// <param name="witsIds"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(IEnumerable<ParameterDto> dtos, CancellationToken token);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
namespace Persistence.Services;
namespace Persistence.Services.Interfaces;
/// <summary>
/// Сервис по работе с БД

View File

@ -1,4 +1,4 @@
namespace Persistence.Services;
namespace Persistence.Services.Interfaces;
public interface ITimeSeriesDataObserverService
{

View File

@ -0,0 +1,46 @@
using Persistence.Models;
namespace Persistence.Services.Interfaces;
/// <summary>
/// Сервис для работы с параметрами Wits
/// </summary>
public interface IWitsDataService
{
/// <summary>
/// Получить набор параметров для построения графика
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<WitsDataDto>> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <param name="approxPointsCount"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<WitsDataDto>> GetValuesForGraph(Guid discriminatorId, DateTimeOffset dateFrom, DateTimeOffset dateTo, int approxPointsCount, CancellationToken token);
/// <summary>
/// Сохранить набор параметров
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(IEnumerable<WitsDataDto> dtos, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
namespace Persistence.Services;
namespace Persistence.Services.Interfaces;
public abstract class TimeSeriesDataObserverService : ITimeSeriesDataObserverService
{
}

View File

@ -0,0 +1,174 @@
using System.Text.Json.Serialization;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Persistence.Models;
using Persistence.Repositories;
using Persistence.Services.Interfaces;
using Persistence.Models.Configurations;
using Persistence.Models.Enumerations;
using System.Globalization;
namespace Persistence.Services;
public class WitsDataService : IWitsDataService
{
private readonly IParameterRepository witsDataRepository;
private readonly WitsInfo[] witsInfo;
private const int multiplier = 1000;
private const string witsConfigPath = "Persistence.Services.Config.WitsConfig.json";
public WitsDataService(IParameterRepository witsDataRepository)

IConfiguration configuration можно удалить, он нигде не используется

IConfiguration configuration можно удалить, он нигде не используется
{
this.witsDataRepository = witsDataRepository;
this.witsInfo = GetWitsInfo();
}
public Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{
var result = witsDataRepository.GetDatesRangeAsync(idDiscriminator, token);
return result;
}
public async Task<IEnumerable<WitsDataDto>> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token)
{
var dtos = await witsDataRepository.GetPart(idDiscriminator, dateBegin, take, token);
var result = AdaptToWitsData(dtos);
return result;
}
public async Task<IEnumerable<WitsDataDto>> GetValuesForGraph(Guid discriminatorId, DateTimeOffset dateFrom, DateTimeOffset dateTo,
int approxPointsCount, CancellationToken token)
{
var intervalSec = (dateTo - dateFrom).TotalSeconds;
int? ratio = null;
if (intervalSec > 2 * approxPointsCount)
{
ratio = (int) intervalSec / approxPointsCount;
}
var dtos = await witsDataRepository.GetValuesForGraph(discriminatorId, dateFrom, dateTo, approxPointsCount, ratio, token);
var result = AdaptToWitsData(dtos);
return result;
}
public async Task<int> AddRange(IEnumerable<WitsDataDto> dtos, CancellationToken token)
{
var parameterDtos = dtos.SelectMany(e => e.Values.Select(t => new ParameterDto()
{
DiscriminatorId = e.DiscriminatorId,
ParameterId = EncodeId(t.RecordId, t.ItemId),
Value = t.Value.ToString()!,
Timestamp = e.Timestamped
}));
var result = await witsDataRepository.AddRange(parameterDtos, token);
return result;
}
private int EncodeId(int recordId, int itemId)
{
var resultId = multiplier * recordId + itemId;
return resultId;
}
private int DecodeRecordId(int id)
{
var resultId = id / multiplier;
return resultId;
}
private int DecodeItemId(int id)
{
var resultId = id % multiplier;
return resultId;
}
private IEnumerable<WitsDataDto> AdaptToWitsData(IEnumerable<ParameterDto> dtos)
{
var result = new List<WitsDataDto>();
var witsGroup = dtos
.GroupBy(e => new { e.DiscriminatorId, e.Timestamp });
foreach (var witsInGroup in witsGroup)

Тут тоже лучше разделить на 2 подзапроса:
result.Where(e => e.DiscriminatorId == dto.DiscriminatorId)
.Where(e => e.Timestamped == dto.Timestamp)
.FirstOrDefault();

Тут тоже лучше разделить на 2 подзапроса: result.Where(e => e.DiscriminatorId == dto.DiscriminatorId) .Where(e => e.Timestamped == dto.Timestamp) .FirstOrDefault();
{
var witsDataDto = new WitsDataDto()
{
DiscriminatorId = witsInGroup.Key.DiscriminatorId,
Timestamped = witsInGroup.Key.Timestamp
};
witsDataDto.Values = witsInGroup.Select(e =>
{
var recordId = DecodeRecordId(e.ParameterId);
var itemId = DecodeItemId(e.ParameterId);
return new WitsValueDto()
{
RecordId = recordId,
ItemId = itemId,
Value = ConvertValue(recordId, itemId, e.Value)
};
});
  1. Тут внутрь witsDataDto ничего не добавляется потому что Append возвращает новую последовательность, поэтому писать нужно так:
    witsDataDto.Values = witsDataDto.Values.Append(witsValueDto)

  2. Не совсем понятный код: цикл по dto, потом поиск еще не добавленных элементов внутрь result и Append.
    Это лучше бы отрефакторить. Ниже приложила вариант того, как можно читаемость (по моему мнению) улучшить.

  3. И еще параметр take. Желательно иметь значение по умолчанию, а то если пользователь нечаянно null отправит, то запрос ничего не выдаст

image

1. Тут внутрь witsDataDto ничего не добавляется потому что Append возвращает новую последовательность, поэтому писать нужно так: witsDataDto.Values = witsDataDto.Values.Append(witsValueDto) 2. Не совсем понятный код: цикл по dto, потом поиск еще не добавленных элементов внутрь result и Append. Это лучше бы отрефакторить. Ниже приложила вариант того, как можно читаемость (по моему мнению) улучшить. 3. И еще параметр take. Желательно иметь значение по умолчанию, а то если пользователь нечаянно null отправит, то запрос ничего не выдаст ![image](/attachments/0dc037f6-7cac-40a5-a8cf-2ca8c60acd5c)
result.Add(witsDataDto);
}
return result;
}
private object ConvertValue(int recordId, int itemId, string value)
{
var witsType = witsInfo.FirstOrDefault(e => e.ItemId == itemId
&& e.RecordId == recordId)?.ValueType;
switch(witsType)
{
default:
{
return value;
}
case WitsType.S:
{
var result = Int16.Parse(value);
return result;
}
case WitsType.L:
{
var result = Int32.Parse(value);
return result;
}
case WitsType.F:
{
var result = float.Parse(value, CultureInfo.InvariantCulture);
return result;
}
}
}
private WitsInfo[] GetWitsInfo()
{
var stream = System.Reflection.Assembly.GetExecutingAssembly()
.GetManifestResourceStream(witsConfigPath);
var options = new JsonSerializerOptions
{
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
},
};
var records = JsonSerializer.Deserialize<WitsInfo[]>(stream!, options) ?? [];
return records;
}
}