Merge from dev

This commit is contained in:
Оля Бизюкова 2024-12-09 17:49:14 +05:00
commit 48dd53fce4
40 changed files with 2320 additions and 722 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,
[FromBody] DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
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>();
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

@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Authorization; using System.Net;
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;
using System.Net;
namespace Persistence.API.Controllers; namespace Persistence.API.Controllers;
@ -14,115 +15,115 @@ namespace Persistence.API.Controllers;
[Route("api/[controller]")] [Route("api/[controller]")]
public class TechMessagesController : ControllerBase public class TechMessagesController : ControllerBase
{ {
private readonly ITechMessagesRepository techMessagesRepository; private readonly ITechMessagesRepository techMessagesRepository;
private static readonly Dictionary<int, string> categories = new Dictionary<int, string>() private static readonly Dictionary<int, string> categories = new Dictionary<int, string>()
{ {
{ 0, "System" }, { 0, "System" },
{ 1, "Авария" }, { 1, "Авария" },
{ 2, "Предупреждение" }, { 2, "Предупреждение" },
{ 3, "Инфо" }, { 3, "Инфо" },
{ 4, "Прочее" } { 4, "Прочее" }
}; };
public TechMessagesController(ITechMessagesRepository techMessagesRepository) public TechMessagesController(ITechMessagesRepository techMessagesRepository)
{ {
this.techMessagesRepository = techMessagesRepository; this.techMessagesRepository = techMessagesRepository;
} }
/// <summary> /// <summary>
/// Получить список технологических сообщений в виде страницы /// Получить список технологических сообщений в виде страницы
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="request"></param>
/// <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);
return Ok(result); return Ok(result);
} }
/// <summary> /// <summary>
/// Получить статистику по системам /// Получить статистику по системам
/// </summary> /// </summary>
/// <param name="autoDrillingSystem"></param> /// <param name="autoDrillingSystem"></param>
/// <param name="categoryIds"></param> /// <param name="categoryIds"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("statistics")] [HttpGet("statistics")]
public async Task<ActionResult<IEnumerable<MessagesStatisticDto>>> GetStatistics([FromQuery] IEnumerable<string> autoDrillingSystem, [FromQuery] IEnumerable<int> categoryIds, CancellationToken token) public async Task<ActionResult<IEnumerable<MessagesStatisticDto>>> GetStatistics([FromQuery] IEnumerable<string> autoDrillingSystem, [FromQuery] IEnumerable<int> categoryIds, CancellationToken token)
{ {
var result = await techMessagesRepository.GetStatistics(autoDrillingSystem, categoryIds, token); var result = await techMessagesRepository.GetStatistics(autoDrillingSystem, categoryIds, token);
return Ok(result); return Ok(result);
} }
/// <summary> /// <summary>
/// Получить список всех систем /// Получить список всех систем
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("systems")] [HttpGet("systems")]
public async Task<ActionResult<Dictionary<string, int>>> GetSystems(CancellationToken token) public async Task<ActionResult<Dictionary<string, int>>> GetSystems(CancellationToken token)
{ {
var result = await techMessagesRepository.GetSystems(token); var result = await techMessagesRepository.GetSystems(token);
return Ok(result); return Ok(result);
} }
/// <summary> /// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории /// Получить диапазон дат, для которых есть данные в репозитории
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("range")] [HttpGet("range")]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync(CancellationToken token) public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync(CancellationToken token)
{ {
var result = await techMessagesRepository.GetDatesRangeAsync(token); var result = await techMessagesRepository.GetDatesRangeAsync(token);
return Ok(result); return Ok(result);
} }
/// <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>
[HttpGet("part")] [HttpGet("part")]
public async Task<ActionResult<IEnumerable<SetpointLogDto>>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) public async Task<ActionResult<IEnumerable<SetpointLogDto>>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token)
{ {
var result = await techMessagesRepository.GetPart(dateBegin, take, token); var result = await techMessagesRepository.GetPart(dateBegin, take, token);
return Ok(result); return Ok(result);
} }
/// <summary> /// <summary>
/// Добавить новые технологические сообщения /// Добавить новые технологические сообщения
/// </summary> /// </summary>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange([FromBody] IEnumerable<TechMessageDto> dtos, CancellationToken token) public async Task<IActionResult> AddRange([FromBody] IEnumerable<TechMessageDto> dtos, CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var result = await techMessagesRepository.AddRange(dtos, userId, token); var result = await techMessagesRepository.AddRange(dtos, userId, token);
return CreatedAtAction(nameof(AddRange), result); return CreatedAtAction(nameof(AddRange), result);
} }
/// <summary> /// <summary>
/// Получить словарь категорий /// Получить словарь категорий
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[HttpGet("categories")] [HttpGet("categories")]
public ActionResult<Dictionary<int, string>> GetImportantCategories() public ActionResult<Dictionary<int, string>> GetImportantCategories()
{ {
return Ok(categories); return Ok(categories);
} }
} }

View File

@ -28,12 +28,12 @@ public static class DependencyInjection
c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" }); c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" });
c.MapType<JsonValue>(() => new OpenApiSchema c.MapType<JsonValue>(() => new OpenApiSchema
{ {
AnyOf = AnyOf = new OpenApiSchema[]
[ {
new() {Type = "string", Format = "string" }, new OpenApiSchema {Type = "string", Format = "string" },
new() {Type = "number", Format = "int32" }, new OpenApiSchema {Type = "number", Format = "int32" },
new() {Type = "number", Format = "float" }, new OpenApiSchema {Type = "number", Format = "float" },
] }
}); });
c.CustomOperationIds(e => c.CustomOperationIds(e =>
@ -45,8 +45,8 @@ public static class DependencyInjection
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>(); var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>();
if (needUseKeyCloak) if (needUseKeyCloak)
c.AddKeycloakSecurity(configuration); c.AddKeycloackSecurity(configuration);
else c.AddDefaultSecurity(); 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);
@ -95,7 +95,7 @@ public static class DependencyInjection
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
{ {
@ -135,9 +135,9 @@ public static class DependencyInjection
#endregion #endregion
#region Security (Swagger) #region Security (Swagger)
private static void AddKeycloakSecurity(this SwaggerGenOptions options, IConfiguration configuration) private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{ {
options.AddSecurityDefinition("Keycloak", 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",
@ -147,8 +147,7 @@ public static class DependencyInjection
{ {
Implicit = new OpenApiOAuthFlow Implicit = new OpenApiOAuthFlow
{ {
AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]),
AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]!),
} }
} }
}); });
@ -161,7 +160,7 @@ public static class DependencyInjection
Reference = new OpenApiReference Reference = new OpenApiReference
{ {
Type = ReferenceType.SecurityScheme, Type = ReferenceType.SecurityScheme,
Id = "Keycloak" Id = "Keycloack"
}, },
Scheme = "Bearer", Scheme = "Bearer",
Name = "Bearer", Name = "Bearer",
@ -172,7 +171,7 @@ public static class DependencyInjection
}); });
} }
private static void AddDefaultSecurity(this SwaggerGenOptions options) private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{ {
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{ {

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

@ -14,5 +14,13 @@
"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>
/// Импорт с заменой: удаление старых строк и добавление новых
/// </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,31 +1,32 @@
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
using Refit; using Refit;
namespace Persistence.Client.Clients namespace Persistence.Client.Clients
{ {
/// <summary> /// <summary>
/// Интерфейс клиента для хранения технологических сообщений /// Интерфейс клиента для хранения технологических сообщений
/// </summary> /// </summary>
public interface ITechMessagesClient public interface ITechMessagesClient
{ {
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);
[Get($"{BaseRoute}/systems")] [Get($"{BaseRoute}/systems")]
Task<IApiResponse<IEnumerable<string>>> GetSystems(CancellationToken token); Task<IApiResponse<IEnumerable<string>>> GetSystems(CancellationToken token);
[Get($"{BaseRoute}/range")] [Get($"{BaseRoute}/range")]
Task<IApiResponse<DatesRangeDto>> GetDatesRangeAsync(CancellationToken token); Task<IApiResponse<DatesRangeDto>> GetDatesRangeAsync(CancellationToken token);
[Get($"{BaseRoute}/part")] [Get($"{BaseRoute}/part")]
Task<IApiResponse<IEnumerable<TechMessageDto>>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token); Task<IApiResponse<IEnumerable<TechMessageDto>>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token);
[Get($"{BaseRoute}/statistics")] [Get($"{BaseRoute}/statistics")]
Task<IApiResponse<IEnumerable<MessagesStatisticDto>>> GetStatistics([Query] string autoDrillingSystem, [Query] int categoryId, CancellationToken token); Task<IApiResponse<IEnumerable<MessagesStatisticDto>>> GetStatistics([Query] string autoDrillingSystem, [Query] int categoryId, CancellationToken token);
} }
} }

View File

@ -1,74 +1,75 @@
using Microsoft.Extensions.Configuration; using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using Persistence.Models.Configurations;
using RestSharp;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Persistence.Models.Configurations;
using RestSharp;
namespace Persistence.Client.Helpers; namespace Persistence.Client.Helpers;
public static class ApiTokenHelper public static class ApiTokenHelper
{ {
public static void Authorize(this HttpClient httpClient, IConfiguration configuration) public static void Authorize(this HttpClient httpClient, IConfiguration configuration)
{ {
var authUser = configuration var authUser = configuration
.GetSection(nameof(AuthUser)) .GetSection(nameof(AuthUser))
.Get<AuthUser>()!; .Get<AuthUser>()!;
var needUseKeyCloak = configuration var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak") .GetSection("NeedUseKeyCloak")
.Get<bool>()!; .Get<bool>()!;
var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty; var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty;
var jwtToken = needUseKeyCloak var jwtToken = needUseKeyCloak
? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl) ? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl)
: authUser.CreateDefaultJwtToken(); : authUser.CreateDefaultJwtToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
} }
private static string CreateDefaultJwtToken(this AuthUser authUser) private static string CreateDefaultJwtToken(this AuthUser authUser)
{ {
var nameIdentifier = Guid.NewGuid().ToString(); var nameIdetifier = Guid.NewGuid().ToString();
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
new(ClaimTypes.NameIdentifier, nameIdentifier), new(ClaimTypes.NameIdentifier, nameIdetifier),
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
{ {
Issuer = JwtParams.Issuer, Issuer = JwtParams.Issuer,
Audience = JwtParams.Audience, Audience = JwtParams.Audience,
Subject = new ClaimsIdentity(claims), Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1), Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature) SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature)
}; };
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor); var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token); return tokenHandler.WriteToken(token);
} }
private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl) private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl)
{ {
var restClient = new RestClient(); var restClient = new RestClient();
var request = new RestRequest(keycloakGetTokenUrl, Method.Post); var request = new RestRequest(keycloakGetTokenUrl, Method.Post);
request.AddParameter("username", authUser.Username); request.AddParameter("username", authUser.Username);
request.AddParameter("password", authUser.Password); request.AddParameter("password", authUser.Password);
request.AddParameter("client_id", authUser.ClientId); request.AddParameter("client_id", authUser.ClientId);
request.AddParameter("grant_type", authUser.GrantType); request.AddParameter("grant_type", authUser.GrantType);
var keycloakResponse = restClient.Post(request); var keyCloackResponse = restClient.Post(request);
if (keycloakResponse.IsSuccessful && !String.IsNullOrEmpty(keycloakResponse.Content)) if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content))
{ {
var token = JsonSerializer.Deserialize<JwtToken>(keycloakResponse.Content)!; var token = JsonSerializer.Deserialize<JwtToken>(keyCloackResponse.Content)!;
return token.AccessToken; return token.AccessToken;
} }
return String.Empty; return String.Empty;
} }
} }

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

@ -5,6 +5,7 @@ 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>();
@ -49,5 +50,9 @@ public partial class PersistenceDbContext : DbContext
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity<ChangeLog>()
.Property(e => e.Value)
.HasJsonConversion();
} }
} }

View File

@ -3,3 +3,9 @@
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; }
[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

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

@ -1,288 +1,289 @@
using Microsoft.Extensions.Caching.Memory; using System.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Persistence.Client; 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 System.Net; using Persistence.Models.Requests;
using Xunit; using Xunit;
namespace Persistence.IntegrationTests.Controllers namespace Persistence.IntegrationTests.Controllers
{ {
public class TechMessagesControllerTest : BaseIntegrationTest public class TechMessagesControllerTest : BaseIntegrationTest
{ {
private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey";
private readonly ITechMessagesClient techMessagesClient; private readonly ITechMessagesClient techMessagesClient;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory) public TechMessagesControllerTest(WebAppFactoryFixture factory) : base(factory)
{ {
var scope = factory.Services.CreateScope(); var scope = factory.Services.CreateScope();
var persistenceClientFactory = scope.ServiceProvider var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>(); .GetRequiredService<PersistenceClientFactory>();
techMessagesClient = persistenceClientFactory.GetClient<ITechMessagesClient>(); techMessagesClient = persistenceClientFactory.GetClient<ITechMessagesClient>();
memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>(); memoryCache = scope.ServiceProvider.GetRequiredService<IMemoryCache>();
} }
[Fact] [Fact]
public async Task GetPage_returns_success() public async Task GetPage_returns_success()
{ {
//arrange //arrange
memoryCache.Remove(SystemCacheKey); memoryCache.Remove(SystemCacheKey);
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,
SortSettings = nameof(TechMessage.CategoryId) SortSettings = nameof(TechMessage.CategoryId)
}; };
//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]
public async Task GetPage_AfterSave_returns_success() public async Task GetPage_AfterSave_returns_success()
{ {
//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,
SortSettings = nameof(TechMessage.CategoryId) SortSettings = nameof(TechMessage.CategoryId)
}; };
//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.Equal(dtosCount, response.Content.Count); Assert.Equal(dtosCount, response.Content.Count);
} }
[Fact] [Fact]
public async Task InsertRange_returns_success() public async Task InsertRange_returns_success()
{ {
await InsertRange(); await InsertRange();
} }
[Fact] [Fact]
public async Task InsertRange_returns_BadRequest() public async Task InsertRange_returns_BadRequest()
{ {
//arrange //arrange
var dtos = new List<TechMessageDto>() var dtos = new List<TechMessageDto>()
{ {
new() new TechMessageDto()
{ {
EventId = Guid.NewGuid(), EventId = Guid.NewGuid(),
CategoryId = -1, // < 0 CategoryId = -1, // < 0
Timestamp = DateTimeOffset.UtcNow, Timestamp = DateTimeOffset.UtcNow,
Depth = -1, // < 0 Depth = -1, // < 0
MessageText = string.Empty, // length < 0 MessageText = string.Empty, // length < 0
System = string.Concat(Enumerable.Repeat(nameof(TechMessageDto.System), 100)), // length > 256 System = string.Concat(Enumerable.Repeat(nameof(TechMessageDto.System), 100)), // length > 256
UserId = Guid.NewGuid() UserId = Guid.NewGuid()
} }
}; };
//act //act
var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); var response = await techMessagesClient.AddRange(dtos, new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
} }
[Fact] [Fact]
public async Task GetSystems_returns_success() public async Task GetSystems_returns_success()
{ {
//arrange //arrange
memoryCache.Remove(SystemCacheKey); memoryCache.Remove(SystemCacheKey);
dbContext.CleanupDbSet<TechMessage>(); dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>(); dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
//act //act
var response = await techMessagesClient.GetSystems(new CancellationToken()); var response = await techMessagesClient.GetSystems(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); Assert.Empty(response.Content);
} }
[Fact] [Fact]
public async Task GetSystems_AfterSave_returns_success() public async Task GetSystems_AfterSave_returns_success()
{ {
//arrange //arrange
var dtos = await InsertRange(); var dtos = await InsertRange();
var systems = dtos var systems = dtos
.Select(e => e.System) .Select(e => e.System)
.Distinct() .Distinct()
.ToArray(); .ToArray();
//act //act
var response = await techMessagesClient.GetSystems(new CancellationToken()); var response = await techMessagesClient.GetSystems(new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content); Assert.NotNull(response.Content);
string?[]? content = response.Content?.ToArray(); string?[]? content = response.Content?.ToArray();
Assert.Equal(systems, content); Assert.Equal(systems, content);
} }
[Fact] [Fact]
public async Task GetStatistics_returns_success() public async Task GetStatistics_returns_success()
{ {
//arrange //arrange
memoryCache.Remove(SystemCacheKey); memoryCache.Remove(SystemCacheKey);
dbContext.CleanupDbSet<TechMessage>(); dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>(); dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
var imortantId = 1; var imortantId = 1;
var autoDrillingSystem = nameof(TechMessageDto.System); var autoDrillingSystem = nameof(TechMessageDto.System);
//act //act
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, 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); Assert.Empty(response.Content);
} }
[Fact] [Fact]
public async Task GetStatistics_AfterSave_returns_success() public async Task GetStatistics_AfterSave_returns_success()
{ {
//arrange //arrange
var imortantId = 0; var imortantId = 0;
var autoDrillingSystem = nameof(TechMessageDto.System); var autoDrillingSystem = nameof(TechMessageDto.System);
var dtos = await InsertRange(); var dtos = await InsertRange();
var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem); var filteredDtos = dtos.Where(e => e.CategoryId == imortantId && e.System == autoDrillingSystem);
//act //act
var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken()); var response = await techMessagesClient.GetStatistics(autoDrillingSystem, imortantId, new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content); Assert.NotNull(response.Content);
var categories = response.Content var categories = response.Content
.FirstOrDefault()?.Categories .FirstOrDefault()?.Categories
.FirstOrDefault(e => e.Key == 0).Value; .FirstOrDefault(e => e.Key == 0).Value;
Assert.Equal(filteredDtos.Count(), categories); Assert.Equal(filteredDtos.Count(), categories);
} }
[Fact] [Fact]
public async Task GetDatesRange_returns_success() public async Task GetDatesRange_returns_success()
{ {
//act //act
var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); var response = await techMessagesClient.GetDatesRangeAsync(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.Equal(DateTimeOffset.MinValue, response.Content?.From); //Assert.Equal(DateTimeOffset.MinValue, response.Content?.From);
//Assert.Equal(DateTimeOffset.MaxValue, response.Content?.To); //Assert.Equal(DateTimeOffset.MaxValue, response.Content?.To);
} }
[Fact] [Fact]
public async Task GetDatesRange_AfterSave_returns_success() public async Task GetDatesRange_AfterSave_returns_success()
{ {
//arrange //arrange
await InsertRange(); await InsertRange();
//act //act
var response = await techMessagesClient.GetDatesRangeAsync(new CancellationToken()); var response = await techMessagesClient.GetDatesRangeAsync(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.NotNull(response.Content?.From); Assert.NotNull(response.Content?.From);
Assert.NotNull(response.Content?.To); Assert.NotNull(response.Content?.To);
} }
[Fact] [Fact]
public async Task GetPart_returns_success() public async Task GetPart_returns_success()
{ {
//arrange //arrange
var dateBegin = DateTimeOffset.UtcNow; var dateBegin = DateTimeOffset.UtcNow;
var take = 2; var take = 2;
//act //act
var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); var response = await techMessagesClient.GetPart(dateBegin, take, 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); Assert.Empty(response.Content);
} }
[Fact] [Fact]
public async Task GetPart_AfterSave_returns_success() public async Task GetPart_AfterSave_returns_success()
{ {
//arrange //arrange
var dateBegin = DateTimeOffset.UtcNow; var dateBegin = DateTimeOffset.UtcNow;
var take = 1; var take = 1;
await InsertRange(); await InsertRange();
//act //act
var response = await techMessagesClient.GetPart(dateBegin, take, new CancellationToken()); var response = await techMessagesClient.GetPart(dateBegin, take, 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.NotEmpty(response.Content); Assert.NotEmpty(response.Content);
} }
private async Task<IEnumerable<TechMessageDto>> InsertRange() private async Task<IEnumerable<TechMessageDto>> InsertRange()
{ {
//arrange //arrange
memoryCache.Remove(SystemCacheKey); memoryCache.Remove(SystemCacheKey);
dbContext.CleanupDbSet<TechMessage>(); dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<DrillingSystem>(); dbContext.CleanupDbSet<DrillingSystem>();
var dtos = new List<TechMessageDto>() var dtos = new List<TechMessageDto>()
{ {
new() new TechMessageDto()
{ {
EventId = Guid.NewGuid(), EventId = Guid.NewGuid(),
CategoryId = 1, CategoryId = 1,
Timestamp = DateTimeOffset.UtcNow, Timestamp = DateTimeOffset.UtcNow,
Depth = 1.11, Depth = 1.11,
MessageText = nameof(TechMessageDto.MessageText), MessageText = nameof(TechMessageDto.MessageText),
System = nameof(TechMessageDto.System).ToLower(), System = nameof(TechMessageDto.System).ToLower(),
UserId = Guid.NewGuid() UserId = Guid.NewGuid()
}, },
new() new TechMessageDto()
{ {
EventId = Guid.NewGuid(), EventId = Guid.NewGuid(),
CategoryId = 2, CategoryId = 2,
Timestamp = DateTimeOffset.UtcNow, Timestamp = DateTimeOffset.UtcNow,
Depth = 2.22, Depth = 2.22,
MessageText = nameof(TechMessageDto.MessageText), MessageText = nameof(TechMessageDto.MessageText),
System = nameof(TechMessageDto.System).ToLower(), System = nameof(TechMessageDto.System).ToLower(),
UserId = Guid.NewGuid() UserId = Guid.NewGuid()
} }
}; };
//act //act
var response = await techMessagesClient.AddRange(dtos, new CancellationToken()); var response = await techMessagesClient.AddRange(dtos, new CancellationToken());
//assert //assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal(dtos.Count, response.Content); Assert.Equal(dtos.Count, response.Content);
return dtos; return dtos;
} }
} }
} }

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)
{
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)
{
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)
{
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);
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);
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}
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>());
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)
.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);
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,175 +3,176 @@ 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;
namespace Persistence.Repository.Repositories namespace Persistence.Repository.Repositories
{ {
public class TechMessagesRepository : ITechMessagesRepository public class TechMessagesRepository : ITechMessagesRepository
{ {
private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey"; private static readonly string SystemCacheKey = $"{typeof(Database.Entity.DrillingSystem).FullName}CacheKey";
private const int CacheExpirationInMinutes = 60; private const int CacheExpirationInMinutes = 60;
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
private DbContext db; private DbContext db;
public TechMessagesRepository(DbContext db, IMemoryCache memoryCache) public TechMessagesRepository(DbContext db, IMemoryCache memoryCache)
{ {
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
this.db = db; this.db = db;
} }
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);
var sort = request.SortSettings != string.Empty var sort = request.SortSettings != string.Empty
? request.SortSettings ? request.SortSettings
: nameof(TechMessage.Timestamp); : nameof(TechMessage.Timestamp);
var entities = await query var entities = await query
.SortBy(request.SortSettings) .SortBy(request.SortSettings)
.Skip(request.Skip) .Skip(request.Skip)
.Take(request.Take) .Take(request.Take)
.ToArrayAsync(token); .ToArrayAsync(token);
var dto = new PaginationContainer<TechMessageDto>() var dto = new PaginationContainer<TechMessageDto>()
{ {
Skip = request.Skip, Skip = request.Skip,
Take = request.Take, Take = request.Take,
Count = count, Count = count,
Items = entities.Select(e => e.Adapt<TechMessageDto>()) Items = entities.Select(e => e.Adapt<TechMessageDto>())
}; };
return dto; return dto;
} }
public async Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token) public async Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token)
{ {
var query = GetQueryReadOnly(); var query = GetQueryReadOnly();
var systems = autoDrillingSystem.Select(s => s.ToLower().Trim()); var systems = autoDrillingSystem.Select(s => s.ToLower().Trim());
var result = await query var result = await query
.Where(e => systems.Count() == 0 || systems.Contains(e.System.Name.ToLower().Trim())) .Where(e => systems.Count() == 0 || systems.Contains(e.System.Name.ToLower().Trim()))
.GroupBy(e => e.System.Name, (key, group) => new .GroupBy(e => e.System.Name, (key, group) => new
{ {
System = key, System = key,
Categories = group Categories = group
.Where(g => categoryIds.Count() == 0 || categoryIds.Contains(g.CategoryId)) .Where(g => categoryIds.Count() == 0 || categoryIds.Contains(g.CategoryId))
}) })
.ToArrayAsync(token); .ToArrayAsync(token);
var entities = new List<MessagesStatisticDto>(); var entities = new List<MessagesStatisticDto>();
foreach (var e in result) foreach (var e in result)
{ {
var categories = e.Categories var categories = e.Categories
.GroupBy(g => g.CategoryId) .GroupBy(g => g.CategoryId)
.ToDictionary(c => c.Key, v => v.Count()); .ToDictionary(c => c.Key, v => v.Count());
var entity = new MessagesStatisticDto() var entity = new MessagesStatisticDto()
{ {
System = e.System, System = e.System,
Categories = categories Categories = categories
}; };
entities.Add(entity); entities.Add(entity);
} }
return entities; return entities;
} }
public async Task<IEnumerable<string>> GetSystems(CancellationToken token) public async Task<IEnumerable<string>> GetSystems(CancellationToken token)
{ {
var entities = await GetDrillingSystems(token); var entities = await GetDrillingSystems(token);
var result = entities.Select(e => e.Name); var result = entities.Select(e => e.Name);
return result; return result;
} }
public async Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token) public async Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token)
{ {
var entities = new List<TechMessage>(); var entities = new List<TechMessage>();
foreach (var dto in dtos) foreach (var dto in dtos)
{ {
var entity = dto.Adapt<TechMessage>(); var entity = dto.Adapt<TechMessage>();
var systems = await GetDrillingSystems(token); var systems = await GetDrillingSystems(token);
var systemId = systems.FirstOrDefault(e => e.Name.ToLower().Trim() == dto.System.ToLower().Trim())?.SystemId var systemId = systems.FirstOrDefault(e => e.Name.ToLower().Trim() == dto.System.ToLower().Trim())?.SystemId
?? await CreateDrillingSystem(dto.System, token); ?? await CreateDrillingSystem(dto.System, token);
entity.SystemId = systemId; entity.SystemId = systemId;
entity.UserId = userId; entity.UserId = userId;
entities.Add(entity); entities.Add(entity);
} }
await db.Set<TechMessage>().AddRangeAsync(entities, token); await db.Set<TechMessage>().AddRangeAsync(entities, token);
var result = await db.SaveChangesAsync(token); var result = await db.SaveChangesAsync(token);
return result; return result;
} }
public async Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token) public async Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token)
{ {
var query = GetQueryReadOnly(); var query = GetQueryReadOnly();
var entities = await query var entities = await query
.Where(e => e.Timestamp >= dateBegin) .Where(e => e.Timestamp >= dateBegin)
.Take(take) .Take(take)
.ToArrayAsync(token); .ToArrayAsync(token);
var dtos = entities var dtos = entities
.Select(e => e.Adapt<TechMessageDto>()); .Select(e => e.Adapt<TechMessageDto>());
return dtos; return dtos;
} }
public async Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token) public async Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token)
{ {
var query = GetQueryReadOnly() var query = GetQueryReadOnly()
.GroupBy(e => 1) .GroupBy(e => 1)
.Select(group => new .Select(group => new
{ {
Min = group.Min(e => e.Timestamp), Min = group.Min(e => e.Timestamp),
Max = group.Max(e => e.Timestamp), Max = group.Max(e => e.Timestamp),
}); });
var values = await query.FirstOrDefaultAsync(token); var values = await query.FirstOrDefaultAsync(token);
var result = new DatesRangeDto() var result = new DatesRangeDto()
{ {
From = values?.Min ?? DateTimeOffset.MinValue, From = values?.Min ?? DateTimeOffset.MinValue,
To = values?.Max ?? DateTimeOffset.MaxValue To = values?.Max ?? DateTimeOffset.MaxValue
}; };
return result; return result;
} }
private async Task<IEnumerable<Models.DrillingSystemDto>> GetDrillingSystems(CancellationToken token) private async Task<IEnumerable<Models.DrillingSystemDto>> GetDrillingSystems(CancellationToken token)
{ {
var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async f => var systems = await memoryCache.GetOrCreateAsync(SystemCacheKey, async f =>
{ {
f.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationInMinutes); f.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationInMinutes);
var query = db.Set<Database.Entity.DrillingSystem>(); var query = db.Set<Database.Entity.DrillingSystem>();
var entities = await query.ToListAsync(token); var entities = await query.ToListAsync(token);
var dtos = entities.Select(e => e.Adapt<Models.DrillingSystemDto>()); var dtos = entities.Select(e => e.Adapt<Models.DrillingSystemDto>());
return dtos; return dtos;
}); });
return systems!; return systems!;
} }
private async Task<Guid> CreateDrillingSystem(string name, CancellationToken token) private async Task<Guid> CreateDrillingSystem(string name, CancellationToken token)
{ {
memoryCache.Remove(SystemCacheKey); memoryCache.Remove(SystemCacheKey);
var entity = new Database.Entity.DrillingSystem() var entity = new Database.Entity.DrillingSystem()
{ {
SystemId = default, SystemId = default,
Name = name.ToLower().Trim() Name = name.ToLower().Trim()
}; };
await db.Set<Database.Entity.DrillingSystem>().AddAsync(entity); await db.Set<Database.Entity.DrillingSystem>().AddAsync(entity);
await db.SaveChangesAsync(token); await db.SaveChangesAsync(token);
return entity.SystemId; return entity.SystemId;
} }
} }
} }

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,12 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
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>
/// Ключ записи
/// </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,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);
/// <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);
/// <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);
/// <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,4 +1,5 @@
using Persistence.Models; using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repositories namespace Persistence.Repositories
{ {
@ -13,7 +14,7 @@ namespace Persistence.Repositories
/// <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>
/// Добавление новых сообщений /// Добавление новых сообщений

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);
} }