Setpoint API #1

Merged
on.nemtina merged 10 commits from Setpoint into master 2024-11-25 10:33:36 +05:00
16 changed files with 608 additions and 23 deletions

View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class SetpointController : ControllerBase, ISetpointApi
{
private readonly ISetpointRepository setpointRepository;
public SetpointController(ISetpointRepository setpointRepository)
{
this.setpointRepository = setpointRepository;
}
[HttpGet("current")]
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointRepository.GetCurrent(setpointKeys, token);
return Ok(result);
}
[HttpGet("history")]
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetHistory([FromQuery] IEnumerable<Guid> setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token)
{
var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token);
return Ok(result);
}
[HttpGet("log")]
public async Task<ActionResult<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointRepository.GetLog(setpointKeys, token);
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<int>> Save(Guid setpointKey, object newValue, CancellationToken token)
{
// ToDo: вычитка idUser
await setpointRepository.Save(setpointKey, newValue, 0, token);
return Ok();
}
}
}

View File

@ -0,0 +1,146 @@
// <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("20241118052225_SetpointMigration")]
partial class SetpointMigration
{
/// <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.Model.DataSaub", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
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<int>("TimeStamp")
.HasColumnType("integer")
.HasColumnName("timestamp");
b.Property<string>("User")
.HasColumnType("text")
.HasColumnName("user");
b.Property<double?>("WellDepth")
.HasColumnType("double precision")
.HasColumnName("wellDepth");
b.HasKey("Id");
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<int>("IdUser")
.HasColumnType("integer")
.HasComment("Id автора последнего изменения");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Created");
b.ToTable("Setpoint");
});
#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 SetpointMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Setpoint",
columns: table => new
{
Key = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ"),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата изменения уставки"),
Value = table.Column<object>(type: "jsonb", nullable: false, comment: "Значение уставки"),
IdUser = table.Column<int>(type: "integer", nullable: false, comment: "Id автора последнего изменения")
},
constraints: table =>
{
table.PrimaryKey("PK_Setpoint", x => new { x.Key, x.Created });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Setpoint");
}
}
}

View File

@ -106,6 +106,30 @@ namespace Persistence.Database.Postgres.Migrations
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<int>("IdUser")
.HasColumnType("integer")
.HasComment("Id автора последнего изменения");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Created");
b.ToTable("Setpoint");
});
#pragma warning restore 612, 618
}
}

View File

@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Data.Common;
namespace Persistence.Database.Model;
@ -6,6 +6,8 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext
{
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public PersistenceDbContext()
: base()
{

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Persistence.Database.Model
{
[PrimaryKey(nameof(Key), nameof(Created))]
public class Setpoint
{
[Comment("Ключ")]
public Guid Key { get; set; }
[Column(TypeName = "jsonb"), Comment("Значение уставки")]
public required object Value { get; set; }
[Comment("Дата создания уставки")]
public DateTimeOffset Created { get; set; }
Review

Должно быть "Дата создания уставки"

Должно быть "Дата создания уставки"
[Comment("Id автора последнего изменения")]
public int IdUser { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
public interface IPersistenceDbContext : IDisposable
{
DbSet<DataSaub> DataSaub { get; }
DbSet<Setpoint> Setpoint { get; }
DatabaseFacade Database { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,25 @@
using Persistence.Models;
using Refit;
namespace Persistence.IntegrationTests.Clients
{
/// <summary>
/// Интерфейс для тестирования API, предназначенного для работы с уставками
/// </summary>
public interface ISetpointClient
{
private const string BaseRoute = "/api/setpoint";
[Get($"{BaseRoute}/current")]
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
[Get($"{BaseRoute}/history")]
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, [Query] DateTimeOffset historyMoment);
[Get($"{BaseRoute}/log")]
Task<IApiResponse<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
[Post($"{BaseRoute}/")]
Task<IApiResponse> Save(Guid setpointKey, object newValue);
}
}

View File

@ -0,0 +1,153 @@
using System.Net;
using Persistence.IntegrationTests.Clients;
using Xunit;
namespace Persistence.IntegrationTests.Controllers
{
public class SetpointControllerTest : BaseIntegrationTest
{
private ISetpointClient client;
private class TestObject
{
public string? value1 { get; set; }
public int? value2 { get; set; }
}
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
{
client = factory.GetHttpClient<ISetpointClient>(string.Empty);
}
[Fact]
public async Task GetCurrent_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
//act
var response = await client.GetCurrent(setpointKeys);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetCurrent_AfterSave_returns_success()
{
//arrange
var setpointKey = await Save();
//act
var response = await client.GetCurrent([setpointKey]);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
}
[Fact]
public async Task GetHistory_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
var historyMoment = DateTimeOffset.UtcNow;
//act
var response = await client.GetHistory(setpointKeys, historyMoment);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetHistory_AfterSave_returns_success()
{
//arrange
var setpointKey = await Save();
var historyMoment = DateTimeOffset.UtcNow;
historyMoment = historyMoment.AddDays(1);
//act
var response = await client.GetHistory([setpointKey], historyMoment);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
}
[Fact]
public async Task GetLog_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
//act
var response = await client.GetLog(setpointKeys);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetLog_AfterSave_returns_success()
{
//arrange
var setpointKey = await Save();
//act
var response = await client.GetLog([setpointKey]);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
}
[Fact]
public async Task Save_returns_success()
{
await Save();
}
private async Task<Guid> Save()
{
//arrange
var setpointKey = Guid.NewGuid();
var setpointValue = new TestObject()
{
value1 = "1",
value2 = 2
};
//act
var response = await client.Save(setpointKey, setpointValue);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
return setpointKey;
}
}
}

View File

@ -10,6 +10,7 @@ using Refit;
using RestSharp;
using System.Net.Http.Headers;
using System.Text.Json;
using Persistence.Database.Postgres;
namespace Persistence.IntegrationTests;
public class WebAppFactoryFixture : WebApplicationFactory<Startup>

View File

@ -0,0 +1,28 @@
namespace Persistence.Repository.Data
{
/// <summary>
/// Модель для работы с уставкой
/// </summary>
public class SetpointDto
{
/// <summary>
/// Идентификатор уставки
/// </summary>
public int Id { get; set; }
/// <summary>
/// Значение уставки
/// </summary>
public required object Value { get; set; }
/// <summary>
/// Дата сохранения уставки
/// </summary>
public DateTimeOffset Edit { get; set; }
/// <summary>
/// Ключ пользователя
/// </summary>
public int IdUser { get; set; }
}
}

View File

@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model;
using Persistence.Repositories;
using Persistence.Repository.Data;
@ -15,7 +15,8 @@ public static class DependencyInjection
{
MapsterSetup();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ISetpointRepository, SetpointRepository>();
return services;
}

View File

@ -0,0 +1,73 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.Repository.Repositories
{
public class SetpointRepository : ISetpointRepository
{
private DbContext db;
public SetpointRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query
.Where(e => setpointKeys.Contains(e.Key))
.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<SetpointValueDto>());
return dtos;
}
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query
.Where(e => setpointKeys.Contains(e.Key))
.ToArrayAsync(token);
var filteredEntities = entities
.GroupBy(e => e.Key)
.Select(e => e.OrderBy(o => o.Created))
.Select(e => e.Where(e => e.Created <= historyMoment).Last());
var dtos = filteredEntities
.Select(e => e.Adapt<SetpointValueDto>());
return dtos;
}
public async Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token)
{
Review

Лучше так:
var dtos = entities
.GroupBy(e => e.Key)
.ToDictionary(e => e.Key, v => v.Select(z => z.Adapt()))

Лучше так: var dtos = entities .GroupBy(e => e.Key) .ToDictionary(e => e.Key, v => v.Select(z => z.Adapt<SetpointLogDto>()))
var query = GetQueryReadOnly();
var entities = await query
.Where(e => setpointKeys.Contains(e.Key))
.ToArrayAsync(token);
var dtos = entities
.GroupBy(e => e.Key)
.ToDictionary(e => e.Key, v => v.Select(z => z.Adapt<SetpointLogDto>()));
return dtos;
}
public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token)
{
var entity = new Setpoint()
{
Key = setpointKey,
Value = newValue,
IdUser = idUser,
Created = DateTimeOffset.UtcNow
};
await db.Set<Setpoint>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
}
}

View File

@ -1,4 +1,4 @@
namespace Persistence.Models;
namespace Persistence.Models;
/// <summary>
/// Модель для описания лога уставки
@ -8,8 +8,8 @@ public class SetpointLogDto : SetpointValueDto
/// <summary>
/// Дата сохранения уставки
/// </summary>
public DateTimeOffset DateEdit { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// Ключ пользователя
/// </summary>

View File

@ -1,18 +1,18 @@
namespace Persistence.Models;
namespace Persistence.Models;
/// <summary>
/// Модель для хранения значения уставки
/// </summary>
public class SetpointValueDto
{
/// <summary>
/// Идентификатор уставки
/// <summary>
/// </summary>
public int Id { get; set; }
public Guid Key { get; set; }
/// <summary>
/// Значение уставки
/// </summary>
public object Value { get; set; }
public required object Value { get; set; }
}

View File

@ -1,4 +1,4 @@
using Persistence.Models;
using Persistence.Models;
namespace Persistence.Repositories;
@ -7,23 +7,30 @@ namespace Persistence.Repositories;
/// </summary>
public interface ISetpointRepository
{
/// <summary>
/// Получить значения уставок по набору ключей
/// </summary>
/// <param name="setpointKeys"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token);
/// <summary>
/// Получить значения уставок за определенный момент времени
/// </summary>
/// <param name="setpoitKeys"></param>
/// <param name="historyMoment">дата, на которую получаем данные</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpoitKeys, DateTimeOffset historyMoment, CancellationToken token);
/// <summary>
/// Получить значения уставок за определенный момент времени
/// </summary>
/// <param name="setpointKeys"></param>
/// <param name="historyMoment">дата, на которую получаем данные</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token);
/// <summary>
/// Получить историю изменений значений уставок
/// </summary>
/// <param name="setpoitKeys"></param>
/// <param name="setpointKeys"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpoitKeys, CancellationToken token);
Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token);
/// <summary>
/// Метод сохранения уставки
@ -35,5 +42,5 @@ public interface ISetpointRepository
/// <returns></returns>
/// to do
/// id User учесть в соответствующем методе репозитория
Task<int> Save(Guid setpointKey, int idUser, object newValue, CancellationToken token);
Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token);
}