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.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
namespace Persistence.API.Controllers;
@ -36,7 +37,7 @@ public class TechMessagesController : ControllerBase
/// <param name="token"></param>
/// <returns></returns>
[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);

View File

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

View File

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

View File

@ -13,6 +13,14 @@
"MetadataAddress": "http://192.168.0.10:8321/realms/Persistence/.well-known/openid-configuration",
"Audience": "account",
"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.Requests;
using Refit;
namespace Persistence.Client.Clients
@ -11,7 +12,7 @@ namespace Persistence.Client.Clients
private const string BaseRoute = "/api/techMessages";
[Get($"{BaseRoute}")]
Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] RequestDto request, CancellationToken token);
Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] PaginationRequest request, CancellationToken token);
[Post($"{BaseRoute}")]
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("username", authUser.Username),
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

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
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 />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
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 =>
{
b.Property<DateTimeOffset>("Date")

View File

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Persistence.Database.Entity;
using System.Data.Common;
namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext
{
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()
: base()
{
}
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options)
@ -49,7 +48,11 @@ public partial class PersistenceDbContext : DbContext
.WithMany()
.HasForeignKey(t => t.SystemId)
.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 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
namespace Persistence.Database.Model;
public interface ITimestampedData
{
/// <summary>

View File

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

View File

@ -1,5 +1,7 @@
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories;
using Persistence.Repository.Data;
using Persistence.Repository.Repositories;
@ -9,6 +11,17 @@ public static class DependencyInjection
{
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)
@ -18,6 +31,7 @@ public static class DependencyInjection
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</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 Persistence.Database.Entity;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using Persistence.Repository.Extensions;
@ -24,7 +25,7 @@ namespace Persistence.Repository.Repositories
protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
.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 count = await query.CountAsync(token);

View File

@ -1,45 +1,71 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.API;
/// <summary>
/// Интерфейс для работы с API журнала изменений
/// </summary>
public interface IChangeLogApi<TDto, TChangeLogDto>
where TDto : class, new()
where TChangeLogDto : ChangeLogDto<TDto>
public interface IChangeLogApi : ISyncWithDiscriminatorApi<DataWithWellDepthAndSectionDto>
{
/// <summary>
/// Получение исторических данных на текущую дату
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetChangeLogCurrent(CancellationToken token);
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение исторических данных на определенную дату
/// Получение данных на текущую дату (с пагинацией)
/// </summary>
/// <param name="historyMoment"></param>
/// <param name="idDiscriminator"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <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>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Add(TDto dto, CancellationToken token);
Task<IActionResult> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> AddRange(IEnumerable<TDto> dtos, CancellationToken token);
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
@ -47,7 +73,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Update(TDto dto, CancellationToken token);
Task<IActionResult> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
@ -55,7 +81,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> UpdateRange(IEnumerable<TDto> dtos, CancellationToken token);
Task<IActionResult> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
@ -63,7 +89,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Delete(int id, CancellationToken token);
Task<IActionResult> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
@ -71,5 +97,13 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <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 Persistence.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Persistence.Models.Requests;
namespace Persistence.API;
/// Интерфейс для API, предназначенного для работы с табличными данными
public interface ITableDataApi<TDto, TRequest>
where TDto : class, new()
where TRequest : RequestDto
where TRequest : PaginationRequest
{
/// <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>
public class ChangeLogDto<T> where T: class
public class ChangeLogDto
{
/// <summary>
/// Запись
/// Ключ записи
/// </summary>
public required T Item { get; set; }
public Guid Id { get; set; }
/// <summary>
/// Автор
/// Создатель записи
/// </summary>
public UserDto? Author { get; set; }
public Guid IdAuthor { get; set; }
/// <summary>
/// Автор
/// Пользователь, изменивший запись
/// </summary>
public UserDto? Editor { get; set; }
public Guid? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например, при удалении)
/// Дата устаревания
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id состояния
/// Ключ заменившей записи

Заменившей

Заменившей
/// </summary>
public int IdState { get; set; }
public Guid? IdNext { get; set; }
/// <summary>
/// Id заменяемой записи
/// Объект записи
/// </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>
/// <typeparam name="T"></typeparam>
public class RequestDto
public class PaginationRequest
{
/// <summary>
/// Кол-во записей пропущенных с начала таблицы в запросе от api
/// </summary>
public int Skip { get; set; }
public int Skip { get; set; } = 0;
/// <summary>
/// Кол-во записей в запросе от api
/// </summary>
public int Take { get; set; }
public int Take { get; set; } = 32;
/// <summary>
/// Настройки сортировки
/// Сортировки:
/// Содержат список названий полей сортировки
/// Указать направление сортировки можно через пробел "asc" или "desc"
/// </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.Requests;
namespace Persistence.Repositories;
@ -6,84 +7,81 @@ namespace Persistence.Repositories;
/// Интерфейс для работы с историческими данными
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository<TDto, TChangeLogDto> : ISyncRepository<TDto>
where TDto : class, ITimeSeriesAbstractDto, new()
where TChangeLogDto : ChangeLogDto<TDto>
public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<DataWithWellDepthAndSectionDto>
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="idUser">пользователь, который добавляет</param>
/// <param name="idAuthor">пользователь, который добавляет</param>
/// <param name="idDiscriminator">ключ справочника</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <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);
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="ids"></param>
/// <param name="idEditor"></param>
/// <param name="ids">ключи записей</param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token);
Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Получение дат изменений записей
/// Пометить записи как удаленные
/// </summary>
/// <param name="request"></param>
/// <param name="idEditor"></param>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token);
Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получение измененных записей за определенную дату
/// Очистить и добавить новые
/// </summary>
/// <param name="updateFrom"></param>
/// <param name="idAuthor"></param>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token);
Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение текущих сейчас записей по параметрам
/// Редактирование записей
/// </summary>
/// <param name="request"></param>
/// <param name="idEditor">пользователь, который редактирует</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <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>
/// <typeparam name="TDto"></typeparam>
public interface ISyncRepository<TDto>
where TDto : class, new()
{
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="dateBegin">дата начала</param>
/// <param name="token"></param> /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <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;
@ -7,7 +7,7 @@ namespace Persistence.Repositories;
/// </summary>
public interface ITableDataRepository<TDto, TRequest>
where TDto : class, new()
where TRequest : RequestDto
where TRequest : PaginationRequest
{
/// <summary>
/// Получить страницу списка объектов

View File

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

View File

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