featute/ChangeLog #6

Merged
on.nemtina merged 25 commits from featute/ChangeLog into master 2024-12-09 17:44:48 +05:00
42 changed files with 1977 additions and 557 deletions

View File

@ -0,0 +1,178 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using System.Net;
namespace Persistence.API.Controllers;
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class ChangeLogController : ControllerBase, IChangeLogApi
{
private IChangeLogRepository repository;
public ChangeLogController(IChangeLogRepository repository)
{
this.repository = repository;
}
[HttpPost("{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]

Не тот хттп код
И дальше тоже есть.

Не тот хттп код И дальше тоже есть.
public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator,

idDiscriminator лучше сделать частью route

idDiscriminator лучше сделать частью route
[FromBody] DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
Review

Тут Да же проблема что и с сообщениями. Id системы, которая нам эти данные отправила, сюда не очень подходит

Тут Да же проблема что и с сообщениями. Id системы, которая нам эти данные отправила, сюда не очень подходит
Review

Решили пока оставить, как есть

Решили пока оставить, как есть
var result = await repository.AddRange(userId, idDiscriminator, [dto], token);
return CreatedAtAction(nameof(Add), result);
}
[HttpPost("range/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
[HttpDelete]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(Guid id, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
Review

Если метод repository.MarkAsDeleted вернет 0 (удаляемая запись отсутствует), то методы delete должны возвращать NoContent

Если метод repository.MarkAsDeleted вернет 0 (удаляемая запись отсутствует), то методы delete должны возвращать NoContent
var result = await repository.MarkAsDeleted(userId, [id], token);
return Ok(result);
}
[HttpDelete("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, ids, token);
return Ok(result);
}
[HttpPost("replace/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token);
return Ok(result);
}
[HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Update(
DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, [dto], token);
return Ok(result);
}
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange(
IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token);
return Ok(result);
}
[HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent(
[FromRoute] Guid idDiscriminator,
[FromQuery] SectionPartRequest filterRequest,
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
[HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset moment,
[FromQuery] SectionPartRequest filterRequest,
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
[HttpGet("history/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetChangeLogForDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset dateBegin,
DateTimeOffset dateEnd,
CancellationToken token)
{
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return Ok(result);
}
[HttpGet("datesChange/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesChange(idDiscriminator, token);
return Ok(result);
}
[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)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
return Ok(result);
}
[HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{

Нужно добавить все коды возврата в аттрибуты метода, чтобы в сваггере они отображались и пользователи знали что при обработке ответа такой код может прилететь и им его как-то надо обработать.

Нужно добавить все коды возврата в аттрибуты метода, чтобы в сваггере они отображались и пользователи знали что при обработке ответа такой код может прилететь и им его как-то надо обработать.
var result = await repository.GetDatesRange(idDiscriminator, token);
if (result is null)
return NoContent();
return Ok(result);
}
}

View File

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories; using Persistence.Repositories;
namespace Persistence.API.Controllers; namespace Persistence.API.Controllers;
@ -36,7 +37,7 @@ public class TechMessagesController : ControllerBase
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet] [HttpGet]
public async Task<ActionResult<PaginationContainer<TechMessageDto>>> GetPage([FromQuery] RequestDto request, CancellationToken token) public async Task<ActionResult<PaginationContainer<TechMessageDto>>> GetPage([FromQuery] PaginationRequest request, CancellationToken token)
{ {
var result = await techMessagesRepository.GetPage(request, token); var result = await techMessagesRepository.GetPage(request, token);

View File

@ -1,5 +1,3 @@
using System.Reflection;
using System.Text.Json.Nodes;
using Mapster; using Mapster;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -9,17 +7,19 @@ using Persistence.Database.Entity;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Configurations; using Persistence.Models.Configurations;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
namespace Persistence.API; namespace Persistence.API;
public static class DependencyInjection public static class DependencyInjection
{ {
public static void MapsterSetup() public static void MapsterSetup()
{ {
TypeAdapterConfig.GlobalSettings.Default.Config TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>() .ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId); .Ignore(dest => dest.System, dest => dest.SystemId);
} }
public static void AddSwagger(this IServiceCollection services, IConfiguration configuration) public static void AddSwagger(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddSwaggerGen(c => services.AddSwaggerGen(c =>
@ -43,162 +43,162 @@ public static class DependencyInjection
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" }); c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" });
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>(); var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>();
if (needUseKeyCloak) if (needUseKeyCloak)
c.AddKeycloackSecurity(configuration); c.AddKeycloackSecurity(configuration);
else c.AddDefaultSecurity(configuration); else c.AddDefaultSecurity(configuration);
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
var includeControllerXmlComment = true; var includeControllerXmlComment = true;
c.IncludeXmlComments(xmlPath, includeControllerXmlComment); c.IncludeXmlComments(xmlPath, includeControllerXmlComment);
}); });
} }
#region Authentication #region Authentication
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
{ {
var needUseKeyCloak = configuration var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak") .GetSection("NeedUseKeyCloak")
.Get<bool>(); .Get<bool>();
if (needUseKeyCloak) if (needUseKeyCloak)
services.AddKeyCloakAuthentication(configuration); services.AddKeyCloakAuthentication(configuration);
else services.AddDefaultAuthentication(configuration); else services.AddDefaultAuthentication(configuration);
} }
private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration) private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.Audience = configuration["Authentication:Audience"]; options.Audience = configuration["Authentication:Audience"];
options.MetadataAddress = configuration["Authentication:MetadataAddress"]!; options.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidIssuer = configuration["Authentication:ValidIssuer"], ValidIssuer = configuration["Authentication:ValidIssuer"],
}; };
}); });
} }
private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = true, ValidateIssuer = true,
ValidIssuer = JwtParams.Issuer, ValidIssuer = JwtParams.Issuer,
ValidateAudience = true, ValidateAudience = true,
ValidAudience = JwtParams.Audience, ValidAudience = JwtParams.Audience,
ValidateLifetime = true, ValidateLifetime = true,
IssuerSigningKey = JwtParams.SecurityKey, IssuerSigningKey = JwtParams.SecurityKey,
ValidateIssuerSigningKey = false ValidateIssuerSigningKey = false,
}; };
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
OnMessageReceived = context => OnMessageReceived = context =>
{ {
var accessToken = context.Request.Headers["Authorization"] var accessToken = context.Request.Headers["Authorization"]
.ToString() .ToString()
.Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty) .Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty)
.Trim(); .Trim();
context.Token = accessToken; context.Token = accessToken;
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnTokenValidated = context => OnTokenValidated = context =>
{ {
var username = context.Principal?.Claims var username = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "username")?.Value; .FirstOrDefault(e => e.Type == "username")?.Value;
var password = context.Principal?.Claims var password = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "password")?.Value; .FirstOrDefault(e => e.Type == "password")?.Value;
var keyCloakUser = configuration var keyCloakUser = configuration
.GetSection(nameof(AuthUser)) .GetSection(nameof(AuthUser))
.Get<AuthUser>()!; .Get<AuthUser>()!;
if (username != keyCloakUser.Username || password != keyCloakUser.Password) if (username != keyCloakUser.Username || password != keyCloakUser.Password)
{ {
context.Fail("username or password did not match"); context.Fail("username or password did not match");
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
}); });
} }
#endregion #endregion
#region Security (Swagger) #region Security (Swagger)
private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration) private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{ {
options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme
{ {
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'", Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization", Name = "Authorization",
In = ParameterLocation.Header, In = ParameterLocation.Header,
Type = SecuritySchemeType.OAuth2, Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows Flows = new OpenApiOAuthFlows
{ {
Implicit = new OpenApiOAuthFlow Implicit = new OpenApiOAuthFlow
{ {
AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]), AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]),
} }
} }
}); });
options.AddSecurityRequirement(new OpenApiSecurityRequirement() options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{ {
{ {
new OpenApiSecurityScheme new OpenApiSecurityScheme
{ {
Reference = new OpenApiReference Reference = new OpenApiReference
{ {
Type = ReferenceType.SecurityScheme, Type = ReferenceType.SecurityScheme,
Id = "Keycloack" Id = "Keycloack"
}, },
Scheme = "Bearer", Scheme = "Bearer",
Name = "Bearer", Name = "Bearer",
In = ParameterLocation.Header, In = ParameterLocation.Header,
}, },
new List<string>() new List<string>()
} }
}); });
} }
private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration) private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{ {
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{ {
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'", Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization", Name = "Authorization",
In = ParameterLocation.Header, In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey, Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer", Scheme = "Bearer",
}); });
options.AddSecurityRequirement(new OpenApiSecurityRequirement() options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{ {
{ {
new OpenApiSecurityScheme new OpenApiSecurityScheme
{ {
Reference = new OpenApiReference Reference = new OpenApiReference
{ {
Type = ReferenceType.SecurityScheme, Type = ReferenceType.SecurityScheme,
Id = "Bearer" Id = "Bearer"
}, },
Scheme = "oauth2", Scheme = "oauth2",
Name = "Bearer", Name = "Bearer",
In = ParameterLocation.Header, In = ParameterLocation.Header,
}, },
new List<string>() new List<string>()
} }
}); });
} }
#endregion #endregion
} }

View File

@ -22,6 +22,7 @@ public class Startup
services.AddSwagger(Configuration); services.AddSwagger(Configuration);
services.AddInfrastructure(); services.AddInfrastructure();
services.AddPersistenceDbContext(Configuration); services.AddPersistenceDbContext(Configuration);
services.AddAuthorization();
services.AddJWTAuthentication(Configuration); services.AddJWTAuthentication(Configuration);
services.AddMemoryCache(); services.AddMemoryCache();
@ -41,6 +42,9 @@ public class Startup
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
} }
app.UseHttpsRedirection();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@ -13,6 +13,14 @@
"MetadataAddress": "http://192.168.0.10:8321/realms/Persistence/.well-known/openid-configuration", "MetadataAddress": "http://192.168.0.10:8321/realms/Persistence/.well-known/openid-configuration",
"Audience": "account", "Audience": "account",
"ValidIssuer": "http://192.168.0.10:8321/realms/Persistence", "ValidIssuer": "http://192.168.0.10:8321/realms/Persistence",
"AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth" "AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth"
},
"NeedUseKeyCloak": false,
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
} }
} }

View File

@ -0,0 +1,106 @@
using Persistence.Models;
using Persistence.Models.Requests;
using Refit;
namespace Persistence.Client.Clients;
/// <summary>
/// Интерфейс для тестирования API, предназначенного для работы с записями ChangeLod
/// </summary>
public interface IChangeLogClient
{
private const string BaseRoute = "/api/ChangeLog";
/// <summary>
Review

лишние комменты

лишние комменты
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <returns></returns>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <returns></returns>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<DataWithWellDepthAndSectionDto>>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
[Query] SectionPartRequest filterRequest,
[Query] PaginationRequest paginationRequest);
/// <summary>
/// Получение исторических данных за определенный период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <returns></returns>
[Get($"{BaseRoute}/history/{{idDiscriminator}}")]
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <returns></returns>
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <returns></returns>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Обновить одну запись
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(DataWithWellDepthAndSectionDto dto);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <returns></returns>
[Put($"{BaseRoute}/range")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[Delete($"{BaseRoute}/range")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <returns></returns>
[Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator);
}

View File

@ -1,4 +1,5 @@
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
using Refit; using Refit;
namespace Persistence.Client.Clients namespace Persistence.Client.Clients
@ -11,7 +12,7 @@ namespace Persistence.Client.Clients
private const string BaseRoute = "/api/techMessages"; private const string BaseRoute = "/api/techMessages";
[Get($"{BaseRoute}")] [Get($"{BaseRoute}")]
Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] RequestDto request, CancellationToken token); Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] PaginationRequest request, CancellationToken token);
[Post($"{BaseRoute}")] [Post($"{BaseRoute}")]
Task<IApiResponse<int>> AddRange([Body] IEnumerable<TechMessageDto> dtos, CancellationToken token); Task<IApiResponse<int>> AddRange([Body] IEnumerable<TechMessageDto> dtos, CancellationToken token);

View File

@ -36,7 +36,8 @@ public static class ApiTokenHelper
new("client_id", authUser.ClientId), new("client_id", authUser.ClientId),
new("username", authUser.Username), new("username", authUser.Username),
new("password", authUser.Password), new("password", authUser.Password),
new("grant_type", authUser.GrantType) new("grant_type", authUser.GrantType),
new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
}; };
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Npgsql;
namespace Persistence.Database.Model; namespace Persistence.Database.Model;

View File

@ -0,0 +1,169 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20241126071115_Add_ChangeLog")]
partial class Add_ChangeLog
{
/// <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.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasColumnName("DepthEnd");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasColumnName("DepthStart");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasColumnName("IdAuthor");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasColumnName("IdDiscriminator");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasColumnName("IdEditor");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasColumnName("IdNext");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasColumnName("IdSection");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<IDictionary<string, object>>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
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");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class Add_ChangeLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ChangeLog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false),
IdEditor = table.Column<Guid>(type: "uuid", nullable: true),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
IdNext = table.Column<Guid>(type: "uuid", nullable: true),
DepthStart = table.Column<double>(type: "double precision", nullable: false),
DepthEnd = table.Column<double>(type: "double precision", nullable: false),
IdSection = table.Column<Guid>(type: "uuid", nullable: false),
Value = table.Column<IDictionary<string, object>>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChangeLog", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChangeLog");
}
}
}

View File

@ -1,5 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -107,6 +108,59 @@ namespace Persistence.Database.Postgres.Migrations
}); });
}); });
modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasColumnName("DepthEnd");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasColumnName("DepthStart");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasColumnName("IdAuthor");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasColumnName("IdDiscriminator");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasColumnName("IdEditor");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasColumnName("IdNext");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasColumnName("IdSection");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<IDictionary<string, object>>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{ {
b.Property<DateTimeOffset>("Date") b.Property<DateTimeOffset>("Date")

View File

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
using Persistence.Database.Entity; using Persistence.Database.Entity;
using System.Data.Common;
namespace Persistence.Database.Model; namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext public partial class PersistenceDbContext : DbContext
{ {
public DbSet<DataSaub> DataSaub => Set<DataSaub>(); public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<Setpoint> Setpoint => Set<Setpoint>(); public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>(); public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>(); public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
public PersistenceDbContext() public PersistenceDbContext()
: base() : base()
{ {
} }
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options) public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options)
@ -49,7 +48,11 @@ public partial class PersistenceDbContext : DbContext
.WithMany() .WithMany()
.HasForeignKey(t => t.SystemId) .HasForeignKey(t => t.SystemId)
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
}
modelBuilder.Entity<ChangeLog>()
.Property(e => e.Value)
.HasJsonConversion();
}
} }

View File

@ -2,4 +2,10 @@
``` ```
dotnet ef migrations add <MigrationName> --project Persistence.Database.Postgres dotnet ef migrations add <MigrationName> --project Persistence.Database.Postgres
``` ```
## Откатить миграцию
```
dotnet ef migrations remove --project Persistence.Database.Postgres
```
Удаляется последняя созданная миграция.

View File

@ -0,0 +1,46 @@

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Persistence.Models;
namespace Persistence.Database.Model;
/// <summary>
/// Часть записи, описывающая изменение
/// </summary>
public class ChangeLog : IChangeLog, IWithSectionPart
{
[Key, Comment("Ключ записи")]
public Guid Id { get; set; }
Review

Договаривались в моделях БД не указывать имя колонки, чтобы оно совпадало с именем свойства.
А чтобы модель визуально оставалась узнаваемой как модель решили указывать атрибут комментария..

Договаривались в моделях БД не указывать имя колонки, чтобы оно совпадало с именем свойства. А чтобы модель визуально оставалась узнаваемой как модель решили указывать атрибут комментария..
[Comment("Дискриминатор таблицы")]
public Guid IdDiscriminator { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
[Comment("Редактор")]
public Guid? IdEditor { get; set; }
[Comment("Дата создания записи")]
public DateTimeOffset Creation { get; set; }
[Comment("Дата устаревания (например при удалении)")]
public DateTimeOffset? Obsolete { get; set; }
[Comment("Id заменяющей записи")]
public Guid? IdNext { get; set; }
[Comment("Глубина забоя на дату начала интервала")]
public double DepthStart { get; set; }
[Comment("Глубина забоя на дату окончания интервала")]
public double DepthEnd { get; set; }
[Comment("Ключ секции")]
public Guid IdSection { get; set; }
[Column(TypeName = "jsonb"), Comment("Значение")]
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -0,0 +1,48 @@

namespace Persistence.Database.Model;
/// <summary>
/// Часть записи, описывающая изменение
/// </summary>
public interface IChangeLog
{
/// <summary>

Кажется этот интерфейс уже не нужен

Кажется этот интерфейс уже не нужен

Он все еще не нужен..

Он все еще не нужен..
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public Guid? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id заменяющей записи
/// </summary>
public Guid? IdNext { get; set; }
/// <summary>
/// Дискриминатор таблицы
/// </summary>
public Guid IdDiscriminator { get; set; }
/// <summary>
/// Значение
/// </summary>
public IDictionary<string, object> Value { get; set; }
}

View File

@ -1,10 +1,4 @@
using System; namespace Persistence.Database.Model;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
public interface ITimestampedData public interface ITimestampedData
{ {
/// <summary> /// <summary>

View File

@ -14,4 +14,8 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,351 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
using System.Net;
using Xunit;
namespace Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest
{
private readonly IChangeLogClient client;
private static Random generatorRandomDigits = new Random();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
client = persistenceClientFactory.GetClient<IChangeLogClient>();
}
[Fact]
public async Task ClearAndInsertRange_InEmptyDb()

Этот кейс проверяет на пустой базе и не охватывает логику отмечания существующих в БД данных как удаленные.

Этот кейс проверяет на пустой базе и не охватывает логику отмечания существующих в БД данных как удаленные.
{
// arrange
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2, DateTimeOffset.UtcNow);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
}
[Fact]
public async Task ClearAndInsertRange_InNotEmptyDb()
{
// arrange
var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var idDiscriminator = createdResult.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>());
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(insertedCount*2, result.Content);
}
[Fact]
public async Task Add_returns_success()
{
// arrange
var count = 1;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
// act
var result = await client.Add(idDiscriminator, dto);
// assert
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task AddRange_returns_success()
{
// arrange
var count = 3;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
// act
var result = await client.AddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task Update_returns_success()
{
// arrange
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto);
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
var entity = dbContext.ChangeLog
.Where(x => x.IdDiscriminator == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<DataWithWellDepthAndSectionDto>();
dto.DepthEnd = dto.DepthEnd + 10;
// act
result = await client.Update(dto);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-1);
var dateEnd = DateTimeOffset.UtcNow.AddDays(1);
var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd);
Assert.Equal(HttpStatusCode.OK, changeLogResult.StatusCode);
Assert.NotNull(changeLogResult.Content);
var changeLogDtos = changeLogResult.Content;
var obsoleteDto = changeLogDtos
.Where(e => e.Obsolete.HasValue)
.FirstOrDefault();
var activeDto = changeLogDtos
.Where(e => !e.Obsolete.HasValue)
.FirstOrDefault();
if (obsoleteDto == null || activeDto == null)
{
Assert.Fail();
return;
}
Assert.Equal(activeDto.Id, obsoleteDto.IdNext);
}
[Fact]
public async Task UpdateRange_returns_success()
{
// arrange
var count = 2;
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new DataWithWellDepthAndSectionDto()
{
DepthEnd = c.DepthEnd + 10,
DepthStart = c.DepthStart + 10,
Id = c.Id,
IdSection = c.IdSection,
Value = c.Value
}).ToArray();
// act
var result = await client.UpdateRange(dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(count * 2, result.Content);
}
[Fact]
public async Task Delete_returns_success()
{
// arrange
var dtos = Generate(1, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
var entity = dto.Adapt<ChangeLog>();
dbContext.ChangeLog.Add(entity);
dbContext.SaveChanges();
// act
var result = await client.Delete(entity.Id);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(1, result.Content);
}
[Fact]
public async Task DeleteRange_returns_success()
{
// arrange
var count = 10;
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
// act
var ids = entities.Select(e => e.Id);
var result = await client.DeleteRange(ids);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task GetDatesRange_returns_success()
{
// arrange
var changeLogItems = CreateChangeLogItems(3, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2.OrderBy(e => e.Creation);
// act
var result = await client.GetDatesRange(idDiscriminator);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
var minDate = entities.First().Creation;
var maxDate = entities.Last().Creation;
var expectedMinDate = minDate.ToUniversalTime().ToString();
var actualMinDate = result.Content.From.ToUniversalTime().ToString();
Assert.Equal(expectedMinDate, actualMinDate);
var expectedMaxDate = maxDate.ToUniversalTime().ToString();
var actualMaxDate = result.Content.To.ToUniversalTime().ToString();
Assert.Equal(expectedMaxDate, actualMaxDate);
}
[Fact]
public async Task GetByDate_returns_success()
{
// arrange
//создаем записи
var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
//удаляем все созданные записи за исключением первой и второй
//даты 2-х оставшихся записей должны вернуться в методе GetByDate
var ids = entities.Select(e => e.Id);
var idsToDelete = ids.Skip(2);
var deletedCount = await client.DeleteRange(idsToDelete);
var filterRequest = new SectionPartRequest()
{
DepthStart = 0,
DepthEnd = 1000,
};
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
var restEntities = entities.Where(e => !idsToDelete.Contains(e.Id));
Assert.Equal(restEntities.Count(), result.Content.Count);
var actualIds = restEntities.Select(e => e.Id);
var expectedIds = result.Content.Items.Select(e => e.Id);
Assert.Equivalent(expectedIds, actualIds);
}
[Theory]
[InlineData(5, -15, 15, -20, 20, 10)]
[InlineData(5, -15, -10, -16, -9, 5)]
public async Task GetChangeLogForInterval_returns_success(
int insertedCount,
int daysBeforeNowChangeLog,
int daysAfterNowChangeLog,
int daysBeforeNowFilter,
int daysAfterNowFilter,
int changeLogCount)
{
// arrange
//создаем записи
var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
foreach (var entity in entities)
{
entity.DepthEnd = entity.DepthEnd + 10;
}
var dtos = entities.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>()).ToArray();
await client.UpdateRange(dtos);
//act
var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter);
var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter);
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd);
//assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
Assert.Equal(changeLogCount, result.Content.Count());
}
private static IEnumerable<DataWithWellDepthAndSectionDto> Generate(int count, DateTimeOffset from)
{
for (int i = 0; i < count; i++)
yield return new DataWithWellDepthAndSectionDto()
{
Value = new Dictionary<string, object>()
{
{ "Key", 1 }
},
DepthStart = generatorRandomDigits.Next(1, 5),
DepthEnd = generatorRandomDigits.Next(5, 15),
Id = Guid.NewGuid(),
IdSection = Guid.NewGuid()
};
}
private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange)
{
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
Guid idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.IdDiscriminator = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
return entity;
}).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
return (idDiscriminator, entities);
}
}

View File

@ -5,6 +5,7 @@ using Persistence.Client;
using Persistence.Client.Clients; using Persistence.Client.Clients;
using Persistence.Database.Entity; using Persistence.Database.Entity;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
using Xunit; using Xunit;
namespace Persistence.IntegrationTests.Controllers namespace Persistence.IntegrationTests.Controllers
@ -32,7 +33,7 @@ namespace Persistence.IntegrationTests.Controllers
dbContext.CleanupDbSet<TechMessage>(); dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>(); dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
var requestDto = new RequestDto() var PaginationRequest = new PaginationRequest()
{ {
Skip = 1, Skip = 1,
Take = 2, Take = 2,
@ -40,14 +41,14 @@ namespace Persistence.IntegrationTests.Controllers
}; };
//act //act
var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content); Assert.NotNull(response.Content);
Assert.Empty(response.Content.Items); Assert.Empty(response.Content.Items);
Assert.Equal(requestDto.Skip, response.Content.Skip); Assert.Equal(PaginationRequest.Skip, response.Content.Skip);
Assert.Equal(requestDto.Take, response.Content.Take); Assert.Equal(PaginationRequest.Take, response.Content.Take);
} }
[Fact] [Fact]
@ -56,7 +57,7 @@ namespace Persistence.IntegrationTests.Controllers
//arrange //arrange
var dtos = await InsertRange(); var dtos = await InsertRange();
var dtosCount = dtos.Count(); var dtosCount = dtos.Count();
var requestDto = new RequestDto() var PaginationRequest = new PaginationRequest()
{ {
Skip = 0, Skip = 0,
Take = 2, Take = 2,
@ -64,7 +65,7 @@ namespace Persistence.IntegrationTests.Controllers
}; };
//act //act
var response = await techMessagesClient.GetPage(requestDto, new CancellationToken()); var response = await techMessagesClient.GetPage(PaginationRequest, new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);

View File

@ -1,5 +1,7 @@
using Mapster;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model; using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories; using Persistence.Repositories;
using Persistence.Repository.Data; using Persistence.Repository.Data;
using Persistence.Repository.Repositories; using Persistence.Repository.Repositories;
@ -9,6 +11,17 @@ public static class DependencyInjection
{ {
public static void MapsterSetup() public static void MapsterSetup()
{ {
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<ChangeLog, ChangeLogDto>()
.Map(dest => dest.Value, src => new DataWithWellDepthAndSectionDto()
{
DepthEnd = src.DepthEnd,
DepthStart = src.DepthStart,
IdSection = src.IdSection,
Value = src.Value,
Id = src.Id
});
} }
public static IServiceCollection AddInfrastructure(this IServiceCollection services) public static IServiceCollection AddInfrastructure(this IServiceCollection services)
@ -18,6 +31,7 @@ public static class DependencyInjection
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>(); services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ISetpointRepository, SetpointRepository>(); services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>(); services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>(); services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>(); services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();

View File

@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,82 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repository;
/// <summary>
/// класс с набором методов, необходимых для фильтрации записей
/// </summary>
public static class QueryBuilders
{
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> query, SectionPartRequest request)
where TEntity : class, IWithSectionPart
{
if (request.IdSection.HasValue)
{
query = query.Where(e => e.IdSection == request.IdSection);
}
if (request.DepthStart.HasValue)
{
query = query.Where(e => e.DepthStart >= request.DepthStart);
}
if (request.DepthEnd.HasValue)
{
query = query.Where(e => e.DepthEnd <= request.DepthEnd);
}
return query;
}
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> query,DateTimeOffset momentUtc)
where TEntity : class, IChangeLog
{
momentUtc = momentUtc.ToUniversalTime();
query = query
.Where(e => e.Creation <= momentUtc)
.Where(e => e.Obsolete == null || e.Obsolete >= momentUtc);
return query;
}
public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query,
PaginationRequest request,
Func<TEntity, TDto> Convert,
CancellationToken token)
where TEntity : class, IWithSectionPart
where TDto : class
{
if (String.IsNullOrEmpty(request.SortSettings))
{
query = query
.OrderBy(e => e.IdSection)
.ThenBy(e => e.DepthStart)
.ThenBy(e => e.DepthEnd);
}
else
{
query = query.SortBy(request.SortSettings);
}
var entities = await query
.Skip(request.Skip)
.Take(request.Take)
.ToArrayAsync(token);
var count = await query.CountAsync(token);
var items = entities.Select(Convert);
var result = new PaginationContainer<TDto>
{
Skip = request.Skip,
Take = request.Take,
Items = items,
Count = count
};
return result;
}
}

View File

@ -0,0 +1,259 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using UuidExtensions;
namespace Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
{
Review

db.Set() хоть и не дорогой но не бесплатный:) Получать его для каждой dto как-то не оправданно.

db.Set<ChangeLog>() хоть и не дорогой но не бесплатный:) Получать его для каждой dto как-то не оправданно.
var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto);
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token)
{
Review

Стоит проверить, что количество записей совпадает с количеством ids. Если не совпадает - исключение

Стоит проверить, что количество записей совпадает с количеством ids. Если не совпадает - исключение
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
.Where(s => s.Obsolete == null);
if (query.Count() != ids.Count())
{
throw new ArgumentException("Count of active items not equal count of ids", nameof(ids));
}
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
{

немного смущает название

немного смущает название
var query = db.Set<ChangeLog>()
.Where(s => s.IdDiscriminator == idDiscriminator)
.Where(e => e.Obsolete == null);

Думаю тут будет разумно проверить, что помечаемые записи еще не устарели. И если мы собираемся отредактировать устаревшее, то падаем в исключение.

Думаю тут будет разумно проверить, что помечаемые записи еще не устарели. И если мы собираемся отредактировать устаревшее, то падаем в исключение.
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
private async Task<int> MarkAsObsolete(Guid idEditor, IEnumerable<ChangeLog> entities, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
foreach (var entity in entities)
{
Review

try-catch тут и дальше не нужен. Rollback ролбэк будет вызван финализатором транзакции, если не был вызван комит

try-catch тут и дальше не нужен. Rollback ролбэк будет вызван финализатором транзакции, если не был вызван комит
entity.Obsolete = updateTime;
entity.IdEditor = idEditor;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeleted(idAuthor, idDiscriminator, token);
result += await AddRange(idAuthor, idDiscriminator, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
var updatedIds = dtos.Select(d => d.Id);
var updatedEntities = dbSet
.Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id);
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);

Не тот тип эксепшена.
Лучше использовать ArgumentException, а в middleware его перехватить и вернуть пользователю 400

Не тот тип эксепшена. Лучше использовать ArgumentException, а в middleware его перехватить и вернуть пользователю 400

try-catch тут и дальше не нужен. Rollback ролбэк будет вызван финализатором транзакции, если не был вызван комит

try-catch тут и дальше не нужен. Rollback ролбэк будет вызван финализатором транзакции, если не был вызван комит
foreach (var dto in dtos)
{
var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id);
if (updatedEntity is null)
{
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto);
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.Obsolete = DateTimeOffset.UtcNow;
updatedEntity.IdEditor = idEditor;
}
result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
SectionPartRequest filterRequest,
PaginationRequest paginationRequest,
CancellationToken token)
{
var query = CreateQuery(idDiscriminator);
query = query.Apply(momentUtc);
query = query.Apply(filterRequest);
private IQueryable<ChangeLog> MakeReadQuery(Guid idDiscriminator, DateTimeOffset momentUtc){...}
private IQueryable<ChangeLog> ApplyFilter(IQueryable<ChangeLog> query,  SectionPartRequest request){...}

"Build" - намекает на наличие билдера, а его нет. (можно подумать чтобы его завести:))

``` private IQueryable<ChangeLog> MakeReadQuery(Guid idDiscriminator, DateTimeOffset momentUtc){...} private IQueryable<ChangeLog> ApplyFilter(IQueryable<ChangeLog> query, SectionPartRequest request){...} ``` "Build" - намекает на наличие билдера, а его нет. (можно подумать чтобы его завести:))
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}

Мы не доверяем пользователям и все равно приводим к UTC

Мы не доверяем пользователям и все равно приводим к UTC
private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
return query;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(s => s.IdDiscriminator == idDiscriminator);
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);
var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);
query = createdQuery.Union(editedQuery);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<ChangeLogDto>());
Review

Тут ошибка в часовых поясах.
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);

Тут ошибка в часовых поясах. `var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);`
return dtos;
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
var datesCreateQuery = query
.Select(e => e.Creation)
.Distinct();
var datesCreate = await datesCreateQuery.ToArrayAsync(token);
var datesUpdateQuery = query
.Where(e => e.Obsolete != null)
.Select(e => e.Obsolete!.Value)
Review

result лучше собирать, когда для него уже есть все данные

result лучше собирать, когда для него уже есть все данные
.Distinct();
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct()
.OrderBy(d => d);
return datesOnly;
}
private ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, DataWithWellDepthAndSectionDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
IdDiscriminator = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value,
IdSection = dto.IdSection,

Конвертацию лучше вынести в метод. Так связь с маппером слабже и преобразование единообразное.

Конвертацию лучше вынести в метод. Так связь с маппером слабже и преобразование единообразное.
DepthStart = dto.DepthStart,
DepthEnd = dto.DepthEnd,
};
return entity;
}
public async Task<IEnumerable<DataWithWellDepthAndSectionDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = this.db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(Convert);
return dtos;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.GroupBy(e => 1)
.Select(group => new
{
Min = group.Min(e => e.Creation),
Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation)
? e.Obsolete.Value
: e.Creation),
});
var values = await query.FirstOrDefaultAsync(token);

Не все базы так поймут. Лучше для новых сущностей их генерировать самостоятельно (в идеале по седьмой версии).

Не все базы так поймут. Лучше для новых сущностей их генерировать самостоятельно (в идеале по [седьмой версии](https://steven-giesel.com/blogPost/ea42a518-4d8b-4e08-8f73-e542bdd3b983)).
if (values is null)
{
return null;
}
return new DatesRangeDto
{
From = values.Min,
To = values.Max,
};
}
private DataWithWellDepthAndSectionDto Convert(ChangeLog entity) => entity.Adapt<DataWithWellDepthAndSectionDto>();
}

View File

@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Persistence.Database.Entity; using Persistence.Database.Entity;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories; using Persistence.Repositories;
using Persistence.Repository.Extensions; using Persistence.Repository.Extensions;
@ -24,7 +25,7 @@ namespace Persistence.Repository.Repositories
protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>() protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
.Include(e => e.System); .Include(e => e.System);
public async Task<PaginationContainer<TechMessageDto>> GetPage(RequestDto request, CancellationToken token) public async Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token)
{ {
var query = GetQueryReadOnly(); var query = GetQueryReadOnly();
var count = await query.CountAsync(token); var count = await query.CountAsync(token);

View File

@ -1,45 +1,71 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.API; namespace Persistence.API;
/// <summary> /// <summary>
/// Интерфейс для работы с API журнала изменений /// Интерфейс для работы с API журнала изменений
/// </summary> /// </summary>
public interface IChangeLogApi<TDto, TChangeLogDto> public interface IChangeLogApi : ISyncWithDiscriminatorApi<DataWithWellDepthAndSectionDto>
where TDto : class, new()
where TChangeLogDto : ChangeLogDto<TDto>
{ {
/// <summary> /// <summary>
/// Получение исторических данных на текущую дату /// Импорт с заменой: удаление старых строк и добавление новых
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetChangeLogCurrent(CancellationToken token); Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Получение исторических данных на определенную дату /// Получение данных на текущую дату (с пагинацией)
/// </summary> /// </summary>
/// <param name="historyMoment"></param> /// <param name="idDiscriminator"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<TChangeLogDto>>> GetChangeLogForDate(DateTimeOffset historyMoment, CancellationToken token); Task<IActionResult> GetCurrent(Guid idDiscriminator, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение данных на определенную дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary> /// <summary>
/// Добавить одну запись /// Добавить одну запись
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> Add(TDto dto, CancellationToken token); Task<IActionResult> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить несколько записей
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> AddRange(IEnumerable<TDto> dtos, CancellationToken token); Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Обновить одну запись /// Обновить одну запись
@ -47,7 +73,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> Update(TDto dto, CancellationToken token); Task<IActionResult> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить несколько записей
@ -55,7 +81,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> UpdateRange(IEnumerable<TDto> dtos, CancellationToken token); Task<IActionResult> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Удалить одну запись /// Удалить одну запись
@ -63,7 +89,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> Delete(int id, CancellationToken token); Task<IActionResult> Delete(Guid id, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Удалить несколько записей
@ -71,5 +97,13 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="ids"></param> /// <param name="ids"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> DeleteRange(IEnumerable<int> ids, CancellationToken token); Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetDatesChange(Guid idDiscriminator, CancellationToken token);
} }

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<IActionResult> GetPart(Guid idDiscriminator, DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token);
}

View File

@ -1,17 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using System; using Persistence.Models.Requests;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.API; namespace Persistence.API;
/// Интерфейс для API, предназначенного для работы с табличными данными /// Интерфейс для API, предназначенного для работы с табличными данными
public interface ITableDataApi<TDto, TRequest> public interface ITableDataApi<TDto, TRequest>
where TDto : class, new() where TDto : class, new()
where TRequest : RequestDto where TRequest : PaginationRequest
{ {
/// <summary> /// <summary>
/// Получить страницу списка объектов /// Получить страницу списка объектов

114
Persistence/EFExtensions.cs Normal file
View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Persistence;
public static class EFExtensions
{
struct TypeAcessor
{
public LambdaExpression KeySelector { get; set; }
public MethodInfo OrderBy { get; set; }
public MethodInfo OrderByDescending { get; set; }
public MethodInfo ThenBy { get; set; }
public MethodInfo ThenByDescending { get; set; }
}
private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy");
private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending");
private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy");
private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending");
private static ConcurrentDictionary<Type, Dictionary<string, TypeAcessor>> TypePropSelectors { get; set; } = new();
private static MethodInfo GetExtOrderMethod(string methodName)
=> typeof(System.Linq.Queryable)
.GetMethods()
.Where(m => m.Name == methodName &&
m.IsGenericMethodDefinition &&
m.GetParameters().Length == 2 &&
m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression)))
.Single();
private static Dictionary<string, TypeAcessor> MakeTypeAcessors(Type type)
{
var propContainer = new Dictionary<string, TypeAcessor>();
var properties = type.GetProperties();
foreach (var propertyInfo in properties)
{
var name = propertyInfo.Name.ToLower();
ParameterExpression arg = Expression.Parameter(type, "x");
MemberExpression property = Expression.Property(arg, propertyInfo.Name);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
var typeAccessor = new TypeAcessor
{
KeySelector = selector,
OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType),
OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType),
ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
};
propContainer.Add(name, typeAccessor);
}
return propContainer;
}
/// <summary>
/// Добавить в запрос сортировку по возрастанию или убыванию.
/// Этот метод сбросит ранее наложенные сортировки.
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <param name="query"></param>
/// <param name="propertySort">
/// Свойство сортировки.
/// Состоит из названия свойства (в любом регистре)
/// и опционально указания направления сортировки "asc" или "desc"
/// </param>
/// <example>
/// var query = query("Date desc");
/// </example>
/// <returns>Запрос с примененной сортировкой</returns>
public static IOrderedQueryable<TSource> SortBy<TSource>(
this IQueryable<TSource> query,
string propertySort)
{
var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries);
var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc";
var propertyName = parts[0];
var newQuery = query.SortBy(propertyName, isDesc);
return newQuery;
}
/// <summary>
/// Добавить в запрос сортировку по возрастанию или убыванию
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <param name="query"></param>
/// <param name="propertyName">Название свойства (в любом регистре)</param>
/// <param name="isDesc">Сортировать по убыванию</param>
/// <returns>Запрос с примененной сортировкой</returns>
public static IOrderedQueryable<TSource> SortBy<TSource>(
this IQueryable<TSource> query,
string propertyName,
bool isDesc)
{
var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAcessors);
var propertyNamelower = propertyName.ToLower();
var typeAccessor = typePropSelector[propertyNamelower];
var genericMethod = isDesc
? typeAccessor.OrderByDescending
: typeAccessor.OrderBy;
var newQuery = (IOrderedQueryable<TSource>)genericMethod
.Invoke(genericMethod, new object[] { query, typeAccessor.KeySelector })!;
return newQuery;
}
}

View File

@ -3,40 +3,40 @@ namespace Persistence.Models;
/// <summary> /// <summary>
/// Часть записи описывающая изменение /// Часть записи описывающая изменение
/// </summary> /// </summary>
public class ChangeLogDto<T> where T: class public class ChangeLogDto
{ {
/// <summary> /// <summary>
/// Запись /// Ключ записи
/// </summary> /// </summary>
public required T Item { get; set; } public Guid Id { get; set; }
/// <summary> /// <summary>
/// Автор /// Создатель записи
/// </summary> /// </summary>
public UserDto? Author { get; set; } public Guid IdAuthor { get; set; }
/// <summary> /// <summary>
/// Автор /// Пользователь, изменивший запись
/// </summary> /// </summary>
public UserDto? Editor { get; set; } public Guid? IdEditor { get; set; }
/// <summary> /// <summary>
/// Дата создания записи /// Дата создания
/// </summary> /// </summary>
public DateTimeOffset Creation { get; set; } public DateTimeOffset Creation { get; set; }
/// <summary> /// <summary>
/// Дата устаревания (например, при удалении) /// Дата устаревания
/// </summary> /// </summary>
public DateTimeOffset? Obsolete { get; set; } public DateTimeOffset? Obsolete { get; set; }
/// <summary> /// <summary>
/// Id состояния /// Ключ заменившей записи

Заменившей

Заменившей
/// </summary> /// </summary>
public int IdState { get; set; } public Guid? IdNext { get; set; }
/// <summary> /// <summary>
/// Id заменяемой записи /// Объект записи
/// </summary> /// </summary>
public int? IdPrevious { get; set; } public DataWithWellDepthAndSectionDto Value { get; set; } = default!;
} }

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models;
/// <summary>
/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// </summary>
public class DataWithWellDepthAndSectionDto
{
/// <summary>
Review

пустые конструкторы не нужны

пустые конструкторы не нужны
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid IdSection { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -1,62 +0,0 @@
namespace Persistence.Models;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public interface IChangeLogAbstract
{
/// <summary>
/// Актуальная
/// </summary>
public const int IdStateActual = 0;
/// <summary>
/// Замененная
/// </summary>
public const int IdStateReplaced = 1;
/// <summary>
/// Удаленная
/// </summary>
public const int IdStateDeleted = 2;
/// <summary>
/// Очищено при импорте
/// </summary>
public const int IdCleared = 3;
/// <summary>
/// Ид записи
/// </summary>
public int Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public int IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public int? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// "ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная
/// </summary>
public int IdState { get; set; }
/// <summary>
/// Id заменяемой записи
/// </summary>
public int? IdPrevious { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Persistence.Models;
public interface IWithSectionPart
{
public double DepthStart { get; set; }
public double DepthEnd { get; set; }
public Guid IdSection { get; set; }
}

View File

@ -1,23 +1,25 @@
namespace Persistence.Models; namespace Persistence.Models.Requests;
/// <summary> /// <summary>
/// Контейнер для поддержки постраничного просмотра таблиц /// Контейнер для поддержки постраничного просмотра таблиц
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public class RequestDto public class PaginationRequest
{ {
/// <summary> /// <summary>
/// Кол-во записей пропущенных с начала таблицы в запросе от api /// Кол-во записей пропущенных с начала таблицы в запросе от api
/// </summary> /// </summary>
public int Skip { get; set; } public int Skip { get; set; } = 0;
/// <summary> /// <summary>
/// Кол-во записей в запросе от api /// Кол-во записей в запросе от api
/// </summary> /// </summary>
public int Take { get; set; } public int Take { get; set; } = 32;
/// <summary> /// <summary>
/// Настройки сортировки /// Сортировки:
/// Содержат список названий полей сортировки
/// Указать направление сортировки можно через пробел "asc" или "desc"
/// </summary> /// </summary>
public string SortSettings { get; set; } = string.Empty; public string? SortSettings { get; set; }
} }

View File

@ -0,0 +1,22 @@
namespace Persistence.Models.Requests;
/// <summary>
/// Запрос для фильтрации данных по секции и глубине
/// </summary>
public class SectionPartRequest

Возможно тут было бы проще без наследования

Возможно тут было бы проще без наследования
{
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double? DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double? DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid? IdSection { get; set; }
}

View File

@ -1,165 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Persistence.Models;
using System.Linq;
namespace Persistence.Repositories;
//public abstract class AbstractChangeLogRepository<TEntity, TChangeLogDto, TDto> : IChangeLogRepository<TDto, TChangeLogDto>
// where TDto : class, new()
// where TEntity : class, IChangeLogAbstract
// where TChangeLogDto : ChangeLogDto<TDto>
//{
// private readonly DbContext dbContext;
// protected AbstractChangeLogRepository(DbContext dbContext)
// {
// this.dbContext = dbContext;
// }
// public abstract TEntity Convert(TDto entity);
// public async Task<int> Clear(int idUser,CancellationToken token)
// {
// throw new NotImplementedException();
// //var updateTime = DateTimeOffset.UtcNow;
// ////todo
// //var query = BuildQuery(request);
// //query = query.Where(e => e.Obsolete == null);
// //var entitiesToDelete = await query.ToArrayAsync(token);
// //foreach (var entity in entitiesToDelete)
// //{
// // entity.IdState = IChangeLogAbstract.IdCleared;
// // entity.Obsolete = updateTime;
// // entity.IdEditor = idUser;
// //}
// //var result = await SaveChangesWithExceptionHandling(token);
// //return result;
// }
// public async Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// var result = 0;
// using var transaction = await dbContext.Database.BeginTransactionAsync(token);
// try
// {
// result += await Clear(idUser, token);
// result += await InsertRangeWithoutTransaction(idUser, dtos, token);
// await transaction.CommitAsync(token);
// return result;
// }
// catch
// {
// await transaction.RollbackAsync(token);
// throw;
// }
// }
// public Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public async Task<int> AddRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// using var transaction = dbContext.Database.BeginTransaction();
// try
// {
// var result = await InsertRangeWithoutTransaction(idUser, dtos, token);
// await transaction.CommitAsync(token);
// return result;
// }
// catch
// {
// await transaction.RollbackAsync(token);
// throw;
// }
// }
// protected abstract DatabaseFacade GetDataBase();
// public Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// private async Task<int> InsertRangeWithoutTransaction(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// var result = 0;
// if (dtos.Any())
// {
// var entities = dtos.Select(Convert);
// var creation = DateTimeOffset.UtcNow;
// var dbSet = dbContext.Set<TEntity>();
// foreach (var entity in entities)
// {
// entity.Id = default;
// entity.IdAuthor = idUser;
// entity.Creation = creation;
// entity.IdState = IChangeLogAbstract.IdStateActual;
// entity.IdEditor = null;
// entity.IdPrevious = null;
// entity.Obsolete = null;
// dbSet.Add(entity);
// }
// result += await SaveChangesWithExceptionHandling(token);
// }
// return result;
// }
// private async Task<int> SaveChangesWithExceptionHandling(CancellationToken token)
// {
// var result = await dbContext.SaveChangesAsync(token);
// return result;
// //try
// //{
// // var result = await dbContext.SaveChangesAsync(token);
// // return result;
// //}
// //catch (DbUpdateException ex)
// //{
// // if (ex.InnerException is PostgresException pgException)
// // TryConvertPostgresExceptionToValidateException(pgException);
// // throw;
// //}
// }
// //private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
// //{
// // if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
// // throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
// //}
//}

View File

@ -1,4 +1,5 @@
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repositories; namespace Persistence.Repositories;
@ -6,84 +7,81 @@ namespace Persistence.Repositories;
/// Интерфейс для работы с историческими данными /// Интерфейс для работы с историческими данными
/// </summary> /// </summary>
/// <typeparam name="TDto"></typeparam> /// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository<TDto, TChangeLogDto> : ISyncRepository<TDto> public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<DataWithWellDepthAndSectionDto>
where TDto : class, ITimeSeriesAbstractDto, new()
where TChangeLogDto : ChangeLogDto<TDto>
{ {
/// <summary> /// <summary>
/// Добавление записей /// Добавление записей
/// </summary> /// </summary>
/// <param name="idUser">пользователь, который добавляет</param> /// <param name="idAuthor">пользователь, который добавляет</param>
/// <param name="idDiscriminator">ключ справочника</param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token); Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);

idUser -> idAuthor, Такое имя позволит не читать комментарий. И совпадает с названием свойства Dto

idUser -> idAuthor, Такое имя позволит не читать комментарий. И совпадает с названием свойства Dto
/// <summary>
/// Редактирование записей
/// </summary>
/// <param name="idUser">пользователь, который редактирует</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Добавляет Dto у которых id == 0, изменяет dto у которых id != 0
/// </summary>
/// <param name="idUser">пользователь, который редактирует или добавляет</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Помечает записи как удаленные
/// </summary>
/// <param name="idUser">пользователь, который чистит</param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Clear(int idUser, CancellationToken token);
/// <summary>
/// Очистить и добавить новые
/// </summary>
/// <param name="idUser"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Пометить записи как удаленные /// Пометить записи как удаленные
/// </summary> /// </summary>
/// <param name="idUser"></param> /// <param name="idEditor"></param>
/// <param name="ids"></param> /// <param name="ids">ключи записей</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token); Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token);
/// <summary> /// <summary>
/// Получение дат изменений записей /// Пометить записи как удаленные
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="idEditor"></param>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token); Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token);
/// <summary> /// <summary>
/// Получение измененных записей за определенную дату /// Очистить и добавить новые
/// </summary> /// </summary>
/// <param name="updateFrom"></param> /// <param name="idAuthor"></param>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token); Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Получение текущих сейчас записей по параметрам /// Редактирование записей
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="idEditor">пользователь, который редактирует</param>
/// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token); Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);

DataWithWellDepthAndSectionDto содержит Guid Id, Поэтому Guid idDiscriminator не нужен.

DataWithWellDepthAndSectionDto содержит Guid Id, Поэтому Guid idDiscriminator не нужен.
/// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment">текущий момент времени</param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);

Лучше ForDate -> ForInterval
Кстати в вместо аргументов dateBegin, dateEnd можно использовать DatesRangeDto

Лучше ForDate -> ForInterval Кстати в вместо аргументов dateBegin, dateEnd можно использовать DatesRangeDto
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token);
} }

View File

@ -1,17 +1,25 @@
namespace Persistence.Repositories; using Persistence.Models;
namespace Persistence.Repositories;
/// <summary> /// <summary>
/// Интерфейс по работе с данными /// Интерфейс по работе с данными
/// </summary> /// </summary>
/// <typeparam name="TDto"></typeparam> /// <typeparam name="TDto"></typeparam>
public interface ISyncRepository<TDto> public interface ISyncRepository<TDto>
where TDto : class, new()
{ {
/// <summary> /// <summary>
/// Получить данные, начиная с определенной даты /// Получить данные, начиная с определенной даты
/// </summary> /// </summary>
/// <param name="dateBegin">дата начала</param> /// <param name="dateBegin">дата начала</param>
/// <param name="token"></param> /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token); Task<DatesRangeDto?> GetDatesRange(CancellationToken token);
} }

View File

@ -0,0 +1,28 @@
using Persistence.Models;
namespace Persistence.Repositories;
/// <summary>
/// Интерфейс по работе с данными, у которых есть дискриминатор
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface ISyncWithDiscriminatorRepository<TDto>
{
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="dateBegin">дата начала</param>
/// <param name="token"></param>
/// /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
using Persistence.Models; using Persistence.Models.Requests;
namespace Persistence.Repositories; namespace Persistence.Repositories;
@ -7,7 +7,7 @@ namespace Persistence.Repositories;
/// </summary> /// </summary>
public interface ITableDataRepository<TDto, TRequest> public interface ITableDataRepository<TDto, TRequest>
where TDto : class, new() where TDto : class, new()
where TRequest : RequestDto where TRequest : PaginationRequest
{ {
/// <summary> /// <summary>
/// Получить страницу списка объектов /// Получить страницу списка объектов

View File

@ -1,59 +1,59 @@
using System.Threading.Tasks; using Persistence.Models;
using Persistence.Models; using Persistence.Models.Requests;
namespace Persistence.Repositories namespace Persistence.Repositories
{ {
/// <summary> /// <summary>
/// Интерфейс по работе с технологическими сообщениями /// Интерфейс по работе с технологическими сообщениями
/// </summary> /// </summary>
public interface ITechMessagesRepository public interface ITechMessagesRepository
{ {
/// <summary> /// <summary>
/// Получить страницу списка объектов /// Получить страницу списка объектов
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="request"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<PaginationContainer<TechMessageDto>> GetPage(RequestDto request, CancellationToken token); Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token);
/// <summary> /// <summary>
/// Добавление новых сообщений /// Добавление новых сообщений
/// </summary> /// </summary>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token); Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token);
/// <summary> /// <summary>
/// Получение списка уникальных названий систем АБ /// Получение списка уникальных названий систем АБ
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<string>> GetSystems(CancellationToken token); Task<IEnumerable<string>> GetSystems(CancellationToken token);
/// <summary> /// <summary>
/// Получение количества сообщений по категориям и системам автобурения /// Получение количества сообщений по категориям и системам автобурения
/// </summary> /// </summary>
/// <param name="categoryId">Id Категории важности</param> /// <param name="categoryId">Id Категории важности</param>
/// <param name="autoDrillingSystem">Система автобурения</param> /// <param name="autoDrillingSystem">Система автобурения</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token); Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token);
/// <summary> /// <summary>
/// Получить порцию записей, начиная с заданной даты /// Получить порцию записей, начиная с заданной даты
/// </summary> /// </summary>
/// <param name="dateBegin"></param> /// <param name="dateBegin"></param>
/// <param name="take"></param> /// <param name="take"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary> /// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории /// Получить диапазон дат, для которых есть данные в репозитории
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token); Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
} }
} }

View File

@ -19,11 +19,4 @@ public interface ITimeSeriesBaseRepository<TDto>
double intervalSec = 600d, double intervalSec = 600d,
int approxPointsCount = 1024, int approxPointsCount = 1024,
CancellationToken token = default); CancellationToken token = default);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(CancellationToken token);
} }