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

Merged
on.nemtina merged 9 commits from WitsData into master 2024-12-12 16:11:56 +05:00
20 changed files with 1013 additions and 3 deletions
Showing only changes of commit d67b6c61d8 - Show all commits

View File

@ -0,0 +1,81 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Services.Interfaces;
namespace Persistence.API.Controllers;
[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("datesRange")]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromQuery] int discriminatorId, CancellationToken token)
{
var result = await witsDataService.GetDatesRangeAsync(discriminatorId, token);

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

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

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

discriminatorId лучше добавить в роут
return Ok(result);
}
/// <summary>
/// Получить набор параметров (Wits) для построения графика
/// </summary>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <param name="limit"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("graph")]
public async Task<ActionResult<IEnumerable<WitsDataDto>>> GetValuesForGraph([FromQuery] DateTimeOffset dateFrom, [FromQuery] DateTimeOffset dateTo, [FromQuery] int limit, CancellationToken token)
{
var result = await witsDataService.GetValuesForGraph(dateFrom, dateTo);
return Ok(result);

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

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

View File

@ -8,6 +8,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;
namespace Persistence.API;
@ -55,6 +57,11 @@ public static class DependencyInjection
});
}
public static void AddServices(this IServiceCollection services)
{
services.AddTransient<IWitsDataService, WitsDataService>();
}
#region Authentication
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
{

View File

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

View File

@ -0,0 +1,20 @@
using Persistence.Models;
using Refit;
namespace Persistence.Client.Clients;
public interface IWitsDataClient
{
private const string BaseRoute = "/api/witsData";
[Get($"{BaseRoute}/graph")]
Task<IApiResponse<IEnumerable<WitsDataDto>>> GetValuesForGraph([Query] DateTimeOffset dateFrom, [Query] DateTimeOffset dateTo, [Query] int limit, CancellationToken token);

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

discriminatorId лучше добавить в роут
[Post($"{BaseRoute}/")]
Task<IApiResponse<int>> InsertRange([Body] IEnumerable<WitsDataDto> dtos, CancellationToken token);
[Get($"{BaseRoute}/part")]
Task<IApiResponse<IEnumerable<WitsDataDto>>> GetPart([Query] int discriminatorId, [Query] DateTimeOffset dateBegin, [Query] int take = 24 * 60 * 60, CancellationToken token = default);
[Get($"{BaseRoute}/datesRange")]
Task<IApiResponse<DatesRangeDto>> GetDatesRangeAsync([Query] int 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<int>(type: "integer", 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

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

@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Persistence.Database.Entity;
using System.Data.Common;
namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext
@ -14,6 +12,8 @@ public partial class PersistenceDbContext : DbContext
public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>();
public PersistenceDbContext()
: base()
{

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 int 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

@ -0,0 +1,223 @@
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 = 1;
//act
var response = await witsDataClient.GetDatesRangeAsync(discriminatorId, new CancellationToken());
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
}
[Fact]
public async Task GetPart_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var discriminatorId = 1;
var dateBegin = DateTimeOffset.UtcNow;
var take = 1;
//act
var response = await witsDataClient.GetPart(discriminatorId, dateBegin, take, new CancellationToken());

Лучше в тестах писать не 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 InsertRange();
}
[Fact]
public async Task GetValuesForGraph_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
//act
//assert
}
#region AfterSave
[Fact]
public async Task GetDatesRangeAsync_AfterSave_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var dtos = await InsertRange();
var discriminatorId = dtos.FirstOrDefault()!.DiscriminatorId;
//act
var response = await witsDataClient.GetDatesRangeAsync(discriminatorId, new CancellationToken());
//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
.Select(e => e.Timestamped)
.Min()
.ToString("dd.MM.yyyy-HH:mm:ss");
var actualDateTo = response.Content.From.DateTime
.ToString("dd.MM.yyyy-HH:mm:ss");
Assert.Equal(expectedDateTo, actualDateTo);
}
Review

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

Здесь 2 раза сравнивается минимум с From
[Fact]
public async Task GetPart_AfterSave_returns_success()
{
//arrange
dbContext.CleanupDbSet<ParameterData>();
var dtos = await InsertRange();
var discriminatorId = dtos.FirstOrDefault()!.DiscriminatorId;
var dateBegin = dtos.FirstOrDefault()!.Timestamped;
var take = 1;
//act
var response = await witsDataClient.GetPart(discriminatorId, dateBegin, take, new CancellationToken());
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(take, response.Content.Count());
}
[Fact]
public async Task GetValuesForGraph_AfterSave_returns_success()
{
}
#endregion
Review

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

Здесь желательно проверить не только количество элементов, но и содержание (id, discriminatorId или что-то в этом роде): вдруг вернулся не тот элемент?
#region BadRequest
[Fact]
public async Task InsertRange_returns_BadRequest()
{
//arrange
var dtos = new List<WitsDataDto>()
{
new WitsDataDto()
{
DiscriminatorId = -1, // < 0
Timestamped = DateTimeOffset.UtcNow,
Values = new List<WitsValueDto>()
{
new WitsValueDto()
{
RecordId = -1, // < 0
ItemId = 101, // > 100
Value = "string value"
}
}
}
};
//act

AddRange_returns_BadRequest

AddRange_returns_BadRequest
var response = await witsDataClient.InsertRange(dtos, new CancellationToken());
//assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
#endregion
private async Task<IEnumerable<WitsDataDto>> InsertRange()
{
//arrange
var dtos = new List<WitsDataDto>()
{
new WitsDataDto()
{
DiscriminatorId = 1,
Timestamped = DateTimeOffset.UtcNow,
Values = new List<WitsValueDto>()
{
new WitsValueDto()
{
RecordId = 11,
ItemId = 22,
Value = "string value"
},
new WitsValueDto()
{
RecordId = 11,
ItemId = 27,
Value = 2.22
}
}
},

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

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

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

По сути нет, это на всякий случай - вдруг кому то когда нибудь придёт идея написать в этом контроллере тест и передать в качестве count >100. Хотя, наверное, лучше тогда человеку сразу увидеть ошибку, чем гадать - почему он говорит создать 150 записей, а создаётся 100 ))
new WitsDataDto()
{
DiscriminatorId = 2,
Timestamped = DateTimeOffset.UtcNow,
Values = new List<WitsValueDto>()
{
new WitsValueDto()
{
RecordId = 13,
ItemId = 14,
Value = "string value"
}

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

Лучше new Random() вынести на уровень выше, чтобы в цикле не создавался каждый раз новый экземпляр
}
}
};
//act
var response = await witsDataClient.InsertRange(dtos, new CancellationToken());
//assert
var count = dtos.SelectMany(e => e.Values).Count();
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal(count, response.Content);
return dtos;
}
}

View File

@ -20,6 +20,7 @@ public static class DependencyInjection
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();
return services;
}

View File

@ -0,0 +1,66 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
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(int 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);
var result = new DatesRangeDto()
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`
{
From = values?.Min ?? DateTimeOffset.MinValue,
To = values?.Max ?? DateTimeOffset.MaxValue
};
return result;
}
public async Task<IEnumerable<ParameterDto>> GetPart(int 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 Task<ParameterDto> GetValuesForGraph(DateTimeOffset dateFrom, DateTimeOffset dateTo)
{
throw new NotImplementedException();
}
public async Task<int> InsertRange(IEnumerable<ParameterDto> dtos, CancellationToken token)
{
var entities = dtos.Select(e => e.Adapt<ParameterData>());
await db.Set<ParameterData>().AddRangeAsync(entities, token);

Тут для удобства чтения лучше раздели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)
var result = await db.SaveChangesAsync(token);
return result;
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
namespace Persistence.API;
/// <summary>
/// Интерфейс для 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<ActionResult<IEnumerable<TDto>>> GetPart(int 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<ActionResult<DatesRangeDto>> GetDatesRangeAsync(int idDiscriminator, CancellationToken token);
}

View File

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

View File

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
namespace Persistence.Models;
/// <summary>
/// Модель параметра
/// </summary>
public class ParameterDto
{
/// <summary>
/// Дискриминатор системы
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "Дискриминатор системы не может быть меньше 0")]
public int 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,25 @@
using System.ComponentModel.DataAnnotations;
namespace Persistence.Models;
/// <summary>
/// Группа параметров Wits
/// </summary>
public class WitsDataDto
{
/// <summary>
/// Временная отметка
/// </summary>
public required DateTimeOffset Timestamped { get; set; }
/// <summary>
/// Дискриминатор системы
/// </summary>
[Range(0, int.MaxValue, ErrorMessage = "Дискриминатор системы не может быть меньше 0")]
public required int 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

@ -0,0 +1,38 @@
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(int idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(int idDiscriminator, CancellationToken token);
/// <summary>
/// Получить набор параметров (Wits) для построения графика
/// </summary>
/// <param name="dateFrom"></param>
/// <param name="dateTo"></param>
/// <returns></returns>
Task<ParameterDto> GetValuesForGraph(DateTimeOffset dateFrom, DateTimeOffset dateTo);
/// <summary>
/// Сохранить набор параметров (Wits)
/// </summary>
/// <param name="dtos"></param>
/// <param name="witsIds"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> InsertRange(IEnumerable<ParameterDto> dtos, CancellationToken token);
}

View File

@ -0,0 +1,10 @@
using Persistence.Models;
namespace Persistence.Services.Interfaces;
public interface IWitsDataService
{
Task<DatesRangeDto> GetDatesRangeAsync(int idDiscriminator, CancellationToken token);
Task<IEnumerable<WitsDataDto>> GetPart(int idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token);
Task<WitsDataDto> GetValuesForGraph(DateTimeOffset dateFrom, DateTimeOffset dateTo);
Task<int> InsertRange(IEnumerable<WitsDataDto> dtos, CancellationToken token);
}

View File

@ -0,0 +1,89 @@
using Persistence.Models;
using Persistence.Repositories;
using Persistence.Services.Interfaces;
namespace Persistence.Services;
public class WitsDataService : IWitsDataService
{
private readonly IParameterRepository witsDataRepository;
private const int multiplier = 1000;
public WitsDataService(IParameterRepository witsDataRepository)
{
this.witsDataRepository = witsDataRepository;
}
public Task<DatesRangeDto> GetDatesRangeAsync(int idDiscriminator, CancellationToken token)
{
var result = witsDataRepository.GetDatesRangeAsync(idDiscriminator, token);
return result;
}

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

IConfiguration configuration можно удалить, он нигде не используется
public async Task<IEnumerable<WitsDataDto>> GetPart(int idDiscriminator, DateTimeOffset dateBegin, int take, CancellationToken token)
{
var dtos = await witsDataRepository.GetPart(idDiscriminator, dateBegin, take, token);
var result = new List<WitsDataDto>();
foreach (var dto in dtos)
{
var witsDataDto = result.FirstOrDefault(e => e.DiscriminatorId == dto.DiscriminatorId && e.Timestamped == dto.Timestamp);
if (witsDataDto == null)
{
witsDataDto = new WitsDataDto()
{
DiscriminatorId = dto.DiscriminatorId,
Timestamped = dto.Timestamp
};
result.Add(witsDataDto);
}
var witsValueDto = new WitsValueDto()
{
RecordId = DecodeRecordId(dto.ParameterId),
ItemId = DecodeItemId(dto.ParameterId),
Value = dto.Value
};
witsDataDto.Values.Append(witsValueDto);
}
return result;
}
public Task<WitsDataDto> GetValuesForGraph(DateTimeOffset dateFrom, DateTimeOffset dateTo)
{
throw new NotImplementedException();
}
public async Task<int> InsertRange(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.InsertRange(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;
}
}