Compare commits

..

3 Commits
master ... dev

132 changed files with 1460 additions and 4739 deletions

View File

@ -1,25 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=db:5432;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
},
"AllowedHosts": "*",
"NeedUseKeyCloak": false,
"KeyCloakAuthentication": {
"Audience": "account",
"Host": "http://192.168.0.10:8321/realms/Persistence"
},
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
},
"ClientUrl": "http://localhost:5000/"
}

View File

@ -1,31 +0,0 @@
networks:
persistence:
external: false
services:
db:
image: timescale/timescaledb:latest-pg16
container_name: some-timescaledb-16
restart: always
environment:
- POSTGRES_PASSWORD=postgres
networks:
- persistence
ports:
- "5462:5432"
volumes:
- ./db:/var/lib/postgresql/data
persistence:
image: git.ddrilling.ru/ddrilling/persistence:latest
container_name: persistence
restart: always
depends_on:
- db
networks:
- persistence
ports:
- "1111:8080"
volumes:
- ./appsettings.json:/app/appsettings.json

View File

@ -1,174 +1,133 @@
using DD.Persistence.API;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using System.Net; using System.Net;
using DD.Persistence.Models.Common;
namespace DD.Persistence.API.Controllers;
/// <summary>
/// Контроллер по работе с журналом изменений
/// </summary>
[ApiController] [ApiController]
[Authorize] [Authorize]
[Route("api/[controller]")] [Route("api/[controller]")]
public class ChangeLogController : ControllerBase, IChangeLogApi public class ChangeLogController : ControllerBase, IChangeLogApi
{ {
private readonly ChangeLogService service; private readonly IChangeLogRepository repository;
/// <summary> public ChangeLogController(IChangeLogRepository repository)
/// ctor
/// </summary>
/// <param name="service"></param>
public ChangeLogController(ChangeLogService service)
{ {
this.service = service; this.repository = repository;
} }
/// <summary>
/// Добавить записи в журнал изменений по дискриминатору
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("{idDiscriminator}")] [HttpPost("{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange( public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<IDictionary<string, object>> dtos, [FromBody] ChangeLogValuesDto dto,
string? comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var changeLogCommitRequest = new ChangeLogCommitCreateRequest var result = await repository.AddRange(userId, idDiscriminator, [dto], token);
{
IdAuthor = userId, return CreatedAtAction(nameof(Add), result);
Comment = comment, }
DiscriminatorId = idDiscriminator,
}; [HttpPost("range/{idDiscriminator}")]
var result = await service.AddRange(changeLogCommitRequest, dtos, token); [ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<ChangeLogValuesDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token);
return CreatedAtAction(nameof(AddRange), result); return CreatedAtAction(nameof(AddRange), result);
} }
/// <summary>
/// Удалить записи в журнале изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="ids"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpDelete] [HttpDelete]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteRange(Guid idDiscriminator, IEnumerable<Guid> ids, string comment, CancellationToken token) public async Task<IActionResult> Delete(Guid id, CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, userId, comment); var result = await repository.MarkAsDeleted(userId, [id], token);
var result = await service.MarkAsDeleted(ids, changeLogCommitRequest, token);
return Ok(result); 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);
}
/// <summary>
/// Очистить все записи в журнале изменений (по дискриминатору) и добавить новые
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPost("replace/{idDiscriminator}")] [HttpPost("replace/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange( public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<IDictionary<string, object>> dtos, [FromBody] IEnumerable<ChangeLogValuesDto> dtos,
string comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, userId, comment); var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token);
var result = await service.ClearAndAddRange(changeLogCommitRequest, dtos, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// сохранить изменения в записях журнала изменений
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="comment"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpPut] [HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<IActionResult> Update(
public async Task<IActionResult> UpdateRange( ChangeLogValuesDto dto,
Guid idDiscriminator,
IEnumerable<ChangeLogBaseDto> dtos,
string comment,
CancellationToken token) CancellationToken token)
{ {
var userId = User.GetUserId<Guid>(); var userId = User.GetUserId<Guid>();
var changeLogCommitRequest = new ChangeLogCommitCreateRequest(idDiscriminator, userId, comment); var result = await repository.UpdateRange(userId, [dto], token);
var result = await service.UpdateRange(changeLogCommitRequest, dtos, token);
return Ok(result);
}
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange(
IEnumerable<ChangeLogValuesDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение актуальных записей (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("{idDiscriminator}")] [HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogBaseDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent( public async Task<IActionResult> GetCurrent(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
[FromQuery] PaginationRequest paginationRequest, [FromQuery] PaginationRequest paginationRequest,
CancellationToken token) CancellationToken token)
{ {
var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero); var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token); var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="paginationRequest"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("moment/{idDiscriminator}")] [HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<ChangeLogBaseDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(PaginationContainer<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate( public async Task<IActionResult> GetByDate(
[FromRoute] Guid idDiscriminator, [FromRoute] Guid idDiscriminator,
DateTimeOffset moment, DateTimeOffset moment,
[FromQuery] PaginationRequest paginationRequest, [FromQuery] PaginationRequest paginationRequest,
CancellationToken token) CancellationToken token)
{ {
var result = await service.GetByDate(idDiscriminator, moment, paginationRequest, token); var result = await repository.GetByDate(idDiscriminator, moment, paginationRequest, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history/{idDiscriminator}")] [HttpGet("history/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
@ -178,91 +137,41 @@ public class ChangeLogController : ControllerBase, IChangeLogApi
DateTimeOffset dateEnd, DateTimeOffset dateEnd,
CancellationToken token) CancellationToken token)
{ {
var result = await service.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token); var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesChange/{idDiscriminator}")] [HttpGet("datesChange/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token) public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token)
{ {
var result = await service.GetDatesChange(idDiscriminator, token); var result = await repository.GetDatesChange(idDiscriminator, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получение данных, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("part/{idDiscriminator}")] [HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogBaseDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<ChangeLogValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<ChangeLogBaseDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default) public async Task<ActionResult<IEnumerable<ChangeLogValuesDto>>> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{ {
var result = await service.GetGtDate(idDiscriminator, dateBegin, token); var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
return Ok(result); return Ok(result);
} }
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("datesRange/{idDiscriminator}")] [HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token) public async Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{ {
var result = await service.GetDatesRange(idDiscriminator, token); var result = await repository.GetDatesRange(idDiscriminator, token);
if (result is null) if (result is null)
return NoContent(); return NoContent();
return Ok(result); return Ok(result);
} }
/// <summary>
/// Метод, который возвращает статистику пользователя по количеству изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("statistics")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogStatisticsDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetStatistics([FromQuery] ChangeLogQuery request, CancellationToken token)
{
var result = await service.GetStatistics(request, token);
return Ok(result);
}
/// <summary>
/// Метод, который возвращает историю изменений в разрезе дней
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet("history")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogCommitDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetHistory([FromQuery] ChangeLogQuery request, CancellationToken token)
{
var result = await service.GetHistory(request, token);
return Ok(result);
}
} }

View File

@ -1,5 +1,4 @@
using DD.Persistence.Filter.Models.Abstractions; using DD.Persistence.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using DD.Persistence.Services.Interfaces; using DD.Persistence.Services.Interfaces;
@ -46,7 +45,6 @@ public class TimestampedValuesController : ControllerBase
/// </summary> /// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов</param> /// <param name="discriminatorIds">Набор дискриминаторов</param>
/// <param name="timestampBegin">Фильтр позднее даты</param> /// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param> /// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param> /// <param name="skip"></param>
/// <param name="take"></param> /// <param name="take"></param>
@ -54,14 +52,9 @@ public class TimestampedValuesController : ControllerBase
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(IEnumerable<TimestampedValuesDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)] [ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromQuery] IEnumerable<Guid> discriminatorIds, public async Task<ActionResult<IEnumerable<TimestampedValuesDto>>> Get([FromQuery] IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin, [FromQuery] string[]? columnNames, int skip, int take, CancellationToken token)
DateTimeOffset? timestampBegin,
[FromQuery] TNode? filterTree,
[FromQuery] string[]? columnNames,
int skip, int take,
CancellationToken token)
{ {
var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token); var result = await timestampedValuesService.Get(discriminatorIds, timestampBegin, columnNames, skip, take, token);
return result.Any() ? Ok(result) : NoContent(); return result.Any() ? Ok(result) : NoContent();
} }

View File

@ -25,11 +25,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" /> <ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" /> <ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" /> <ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Docs\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,14 +1,16 @@
using DD.Persistence.Filter.Models.Abstractions; using Mapster;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using DD.Persistence.Services;
using DD.Persistence.Services.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection; using System.Reflection;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using DD.Persistence.Database.Entity;
namespace DD.Persistence.API; namespace DD.Persistence.API;
@ -28,7 +30,6 @@ public static class DependencyInjection
new OpenApiSchema {Type = "number", Format = "float" } new OpenApiSchema {Type = "number", Format = "float" }
] ]
}); });
c.MapType<TNode>(() => new OpenApiSchema { Type = "string" });
c.CustomOperationIds(e => c.CustomOperationIds(e =>
{ {
@ -53,7 +54,6 @@ public static class DependencyInjection
{ {
services.AddTransient<IWitsDataService, WitsDataService>(); services.AddTransient<IWitsDataService, WitsDataService>();
services.AddTransient<ITimestampedValuesService, TimestampedValuesService>(); services.AddTransient<ITimestampedValuesService, TimestampedValuesService>();
services.AddTransient<ChangeLogService>();
} }
#region Authentication #region Authentication

View File

@ -1,359 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" version="24.8.3">
<diagram name="Страница — 1" id="7k5Wemfp-yc9piGHxsiE">
<mxGraphModel dx="2049" dy="1054" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1300" pageHeight="1050" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="_dnhcZFeje3u91oS2JwL-1" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="355" y="930" as="sourcePoint" />
<mxPoint x="355" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-2" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="930" as="sourcePoint" />
<mxPoint x="555" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-4" value="&lt;b&gt;FRONT&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="325" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-5" value="&lt;b&gt;BACK&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="525" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-7" value="&lt;b&gt;CACHE&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="725" y="180" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-8" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="955" y="930" as="sourcePoint" />
<mxPoint x="955" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-9" value="&lt;b&gt;COMMIT REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="880" y="180" width="155" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-32" value="" style="endArrow=none;html=1;rounded=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;align=center;" edge="1" parent="1" target="_dnhcZFeje3u91oS2JwL-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="930" as="sourcePoint" />
<mxPoint x="756.5000000000002" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-52" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="300" as="sourcePoint" />
<mxPoint x="755" y="300" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-53" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-52">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-67" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="310" as="sourcePoint" />
<mxPoint x="955" y="310.83" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-68" value="CreateCommit(userId, message)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-67">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-69" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="955" y="330" as="sourcePoint" />
<mxPoint x="755" y="330" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-70" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-69">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-88" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="930" as="sourcePoint" />
<mxPoint x="1155" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-89" value="&lt;b&gt;CHANGE LOG REPOSITORY&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
<mxGeometry x="1060" y="180" width="185" height="30" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-90" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="340" as="sourcePoint" />
<mxPoint x="555" y="340" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-91" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-90">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-93" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="380" as="sourcePoint" />
<mxPoint x="1155" y="380" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-94" value="AddRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-93">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-96" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="410" as="sourcePoint" />
<mxPoint x="555" y="410" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-97" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-96">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-100" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="355" y="420" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-101" value="CREATED" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-100">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-102" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="555" y="420" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-104" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="290" as="sourcePoint" />
<mxPoint x="555" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-105" value="Insert(items, message) [POST]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-104">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-107" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.13" y="420" as="sourcePoint" />
<mxPoint x="354.13" y="290" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-108" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="520" as="sourcePoint" />
<mxPoint x="555" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-109" value="Update(items, message) [PUT]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-108">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-110" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="530" as="sourcePoint" />
<mxPoint x="755" y="530" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-111" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-110">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-112" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="560" as="sourcePoint" />
<mxPoint x="555" y="560" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-113" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-112">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-114" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="650" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-115" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="610" as="sourcePoint" />
<mxPoint x="1155" y="610" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-116" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-115">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-117" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="640" as="sourcePoint" />
<mxPoint x="555" y="640" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-118" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-117">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-119" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="650" as="sourcePoint" />
<mxPoint x="355" y="650" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-120" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-119">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-121" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="530" as="sourcePoint" />
<mxPoint x="755" y="560" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="550" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-122" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="760" as="sourcePoint" />
<mxPoint x="555" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-123" value="Delete(items, message) [DELETE]" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-122">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-124" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="770" as="sourcePoint" />
<mxPoint x="755" y="770" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-125" value="&lt;span style=&quot;font-size: 12px; text-wrap-mode: wrap;&quot;&gt;GetOrCreate(userId, message)&lt;/span&gt;" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-124">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-126" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="755" y="800" as="sourcePoint" />
<mxPoint x="555" y="800" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-127" value="CommitId" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-126">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-128" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="554.5699999999999" y="890" as="sourcePoint" />
<mxPoint x="554.5699999999999" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-129" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="850" as="sourcePoint" />
<mxPoint x="1155" y="850" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-130" value="UpdateRange(items, commitId)" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-129">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-131" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1155" y="880" as="sourcePoint" />
<mxPoint x="555" y="880" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-132" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-131">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-133" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="555" y="890" as="sourcePoint" />
<mxPoint x="355" y="890" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-134" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-133">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-135" value="" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;endArrow=classic;html=1;curved=0;rounded=0;endSize=8;startSize=8;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="770" as="sourcePoint" />
<mxPoint x="755" y="800" as="targetPoint" />
<Array as="points">
<mxPoint x="775" y="790" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-136" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="650" as="sourcePoint" />
<mxPoint x="354.57" y="520" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-137" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=9;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="354.57" y="890" as="sourcePoint" />
<mxPoint x="354.57" y="760" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-138" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="411" as="sourcePoint" />
<mxPoint x="1155" y="381" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-139" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1155" y="641" as="sourcePoint" />
<mxPoint x="1155" y="611" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-140" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#f8cecc;gradientColor=#ea6b66;strokeColor=#b85450;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="1154.75" y="881" as="sourcePoint" />
<mxPoint x="1154.75" y="851" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-141" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=10;curved=1;targetPerimeterSpacing=3;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;opacity=50;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="755" y="830" as="sourcePoint" />
<mxPoint x="754.93" y="312" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-142" value="" style="endArrow=none;html=1;rounded=0;fillColor=#dae8fc;strokeColor=#6c8ebf;strokeWidth=10;opacity=50;gradientColor=#7ea6e0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165" y="882" as="sourcePoint" />
<mxPoint x="165.22" y="292" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-143" value="&lt;font color=&quot;#330066&quot;&gt;User&lt;/font&gt;" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;align=center;" vertex="1" parent="1">
<mxGeometry x="155" y="180" width="21.5" height="43" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-146" value="" style="endArrow=classic;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="170" y="280" as="sourcePoint" />
<mxPoint x="360" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-147" value="SAVE" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;labelBackgroundColor=none;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-146">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-148" value="" style="endArrow=none;html=1;rounded=0;align=center;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="165.22" y="930" as="sourcePoint" />
<mxPoint x="165.22" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-149" value="" style="endArrow=classic;html=1;rounded=0;dashed=1;align=center;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="355" y="892" as="sourcePoint" />
<mxPoint x="165" y="892" as="targetPoint" />
<Array as="points">
<mxPoint x="265" y="892" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-150" value="OK" style="edgeLabel;resizable=0;html=1;;align=center;verticalAlign=middle;" connectable="0" vertex="1" parent="_dnhcZFeje3u91oS2JwL-149">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-152" value="&lt;h1 style=&quot;margin-top: 0px;&quot;&gt;UML-диаграмма процесса пакетного редактирования&lt;/h1&gt;&lt;p&gt;.&lt;/p&gt;" style="text;html=1;whiteSpace=wrap;overflow=hidden;rounded=0;align=center;" vertex="1" parent="1">
<mxGeometry x="75" y="50" width="1150" height="70" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-154" value="frame" style="shape=umlFrame;whiteSpace=wrap;html=1;pointerEvents=0;" vertex="1" parent="1">
<mxGeometry x="120" y="250" width="1120" height="680" as="geometry" />
</mxCell>
<mxCell id="_dnhcZFeje3u91oS2JwL-156" value="&lt;p style=&quot;line-height: 120%;&quot;&gt;Время жизни кеша задается внутри проекта&lt;/p&gt;" style="shape=note;size=20;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#d0cee2;strokeColor=#56517e;opacity=50;" vertex="1" parent="1">
<mxGeometry x="760" y="660" width="120" height="100" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -48,7 +48,4 @@ Password: 12345
} }
``` ```
## Пакетное редактирование (на примере ChangeLog)
UML-диаграмма процесса редактирования находится по [ссылке](https://git.ddrilling.ru/on.nemtina/persistence/src/branch/master/DD.Persistence.API/Docs/ChangeLog_actions.drawio.xml)

View File

@ -1,6 +1,6 @@
using DD.Persistence.Database; using DD.Persistence.Database.Model;
using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Extensions; using DD.Persistence.Database.Postgres.Extensions;
using DD.Persistence.Repository;
namespace DD.Persistence.API; namespace DD.Persistence.API;
@ -14,7 +14,7 @@ public class Startup
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// AddRange services to the container. // Add services to the container.
services.AddControllers(); services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -28,6 +28,7 @@ COPY ["DD.Persistence/DD.Persistence.csproj", "DD.Persistence/"]
COPY ["DD.Persistence.Database/DD.Persistence.Database.csproj", "DD.Persistence.Database/"] COPY ["DD.Persistence.Database/DD.Persistence.Database.csproj", "DD.Persistence.Database/"]
COPY ["DD.Persistence.Database.Postgres/DD.Persistence.Database.Postgres.csproj", "DD.Persistence.Database.Postgres/"] COPY ["DD.Persistence.Database.Postgres/DD.Persistence.Database.Postgres.csproj", "DD.Persistence.Database.Postgres/"]
COPY ["DD.Persistence.Models/DD.Persistence.Models.csproj", "DD.Persistence.Models/"] COPY ["DD.Persistence.Models/DD.Persistence.Models.csproj", "DD.Persistence.Models/"]
COPY ["DD.Persistence.Repository/DD.Persistence.Repository.csproj", "DD.Persistence.Repository/"]
RUN dotnet restore "./DD.Persistence.App/DD.Persistence.App.csproj" RUN dotnet restore "./DD.Persistence.App/DD.Persistence.App.csproj"

View File

@ -6,7 +6,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True" "DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=postgres;Persist Security Info=True"
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"NeedUseKeyCloak": false, "NeedUseKeyCloak": false,
@ -21,5 +21,5 @@
"grantType": "password", "grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6" "http://schemas.xmlsoap.org/ws/2005/05/identity /claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
}, },
"ClientUrl": "http://localhost:5000/" "PeristenceClientUrl": "http://localhost:5000/"
} }

View File

@ -1,10 +1,10 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using DD.Persistence.Client.Clients.Base; using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Models;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Models.ChangeLog;
namespace DD.Persistence.Client.Clients; namespace DD.Persistence.Client.Clients;
/// <inheritdoc/> /// <inheritdoc/>
@ -19,16 +19,16 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<IDictionary<string, object>> dtos, string comment, CancellationToken token) public async Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{ {
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, comment, token), token); async () => await refitChangeLogClient.ClearAndAddRange(idDiscriminator, dtos, token), token);
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, public async Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment,
PaginationRequest paginationRequest, CancellationToken token) PaginationRequest paginationRequest, CancellationToken token)
{ {
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
@ -47,28 +47,55 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<IDictionary<string, object>> dtos, string comment, CancellationToken token) public async Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, comment, token), token); async () => await refitChangeLogClient.Add(idDiscriminator, dto, token), token);
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> UpdateRange(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> dtos, string comment, CancellationToken token) public async Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.UpdateRange(idDiscriminator, dtos, comment, token), token); async () => await refitChangeLogClient.AddRange(idDiscriminator, dtos, token), token);
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> DeleteRange(Guid idDiscriminator, IEnumerable<Guid> ids, string comment, CancellationToken token) public async Task<int> Update(ChangeLogValuesDto dto, CancellationToken token)
{ {
var result = await ExecutePostResponse( var result = await ExecutePostResponse(
async () => await refitChangeLogClient.DeleteRange(idDiscriminator, ids, comment, token), token); async () => await refitChangeLogClient.Update(dto, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.UpdateRange(dtos, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> Delete(Guid id, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.Delete(id, token), token);
return result;
}
/// <inheritdoc/>
public async Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var result = await ExecutePostResponse(
async () => await refitChangeLogClient.DeleteRange(ids, token), token);
return result; return result;
} }
@ -82,24 +109,6 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
return result; return result;
} }
/// <inheritdoc/>
public async Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(ChangeLogQuery request, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.GetStatistics(request, token), token);
return result!;
}
/// <inheritdoc/>
public async Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token)
{
var result = await ExecuteGetResponse(
async () => await refitChangeLogClient.GetHistory(request, token), token);
return result!;
}
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
@ -107,5 +116,4 @@ public class ChangeLogClient : BaseClient, IChangeLogClient
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
} }

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models.ChangeLog; using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
@ -9,35 +9,48 @@ namespace DD.Persistence.Client.Clients.Interfaces;
/// </summary> /// </summary>
public interface IChangeLogClient : IDisposable public interface IChangeLogClient : IDisposable
{ {
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить одну запись
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dto"></param>
/// <param name="comment"></param> /// <param name="token"></param>
/// <param name="token"></param> /// <returns></returns>
/// <returns></returns> Task<int> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
Task<int> AddRange(Guid idDiscriminator, IEnumerable<IDictionary<string, object>> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых /// Добавить несколько записей
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="comment"></param> /// <param name="token"></param>
/// <param name="token"></param> /// <returns></returns>
/// <returns></returns> Task<int> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<IDictionary<string, object>> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Импорт с заменой: удаление старых строк и добавление новых
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="idDiscriminator"></param>
/// <param name="ids"></param> /// <param name="dtos"></param>
/// <param name="comment"></param> /// <param name="token"></param>
/// <param name="token"></param> /// <returns></returns>
/// <returns></returns> Task<int> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
Task<int> DeleteRange(Guid idDiscriminator, IEnumerable<Guid> ids, string comment, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
/// <summary> /// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией) /// Получение актуальных данных на определенную дату (с пагинацией)
@ -47,7 +60,7 @@ public interface IChangeLogClient : IDisposable
/// <param name="paginationRequest"></param> /// <param name="paginationRequest"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token); Task<PaginationContainer<ChangeLogValuesDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, PaginationRequest paginationRequest, CancellationToken token);
/// <summary> /// <summary>
/// Получение исторических данных за определенный период времени /// Получение исторических данных за определенный период времени
@ -67,29 +80,19 @@ public interface IChangeLogClient : IDisposable
/// <returns></returns> /// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token); Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить одну запись
/// </summary> /// </summary>
/// <param name="idDiscriminator"></param> /// <param name="dto"></param>
/// <param name="dtos"></param> /// <param name="token"></param>
/// <param name="comment"></param> /// <returns></returns>
/// <param name="token"></param> Task<int> Update(ChangeLogValuesDto dto, CancellationToken token);
/// <returns></returns>
Task<int> UpdateRange(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> dtos, string comment, CancellationToken token);
/// <summary> /// <summary>
/// Получение статистики журнала изменений /// Обновить несколько записей
/// </summary> /// </summary>
/// <param name="request"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(ChangeLogQuery request, CancellationToken token); Task<int> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Получение истории по журналу изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token);
} }

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
namespace DD.Persistence.Client.Clients.Interfaces; namespace DD.Persistence.Client.Clients.Interfaces;
@ -24,19 +24,28 @@ public interface ITimestampedValuesClient : IDisposable
/// </summary> /// </summary>
/// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</param> /// <param name="discriminatorIds">Набор дискриминаторов (идентификаторов)</param>
/// <param name="timestampBegin">Фильтр позднее даты</param> /// <param name="timestampBegin">Фильтр позднее даты</param>
/// <param name="filterTree">Кастомный фильтр по набору значений</param>
/// <param name="columnNames">Фильтр свойств набора</param> /// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param> /// <param name="skip"></param>
/// <param name="take"></param> /// <param name="take"></param>
/// <param name="token"></param> /// <param name="token"></param>
Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin, DateTimeOffset? timestampBegin,
string? filterTree,
IEnumerable<string>? columnNames, IEnumerable<string>? columnNames,
int skip, int skip,
int take, int take,
CancellationToken token); CancellationToken token);
/// <summary>
/// Получить данные с фильтрацией для нескольких систем. Значение фильтра null - отключен
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="geTimestamp"></param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary> /// <summary>
/// Получить данные, начиная с заданной отметки времени /// Получить данные, начиная с заданной отметки времени
/// </summary> /// </summary>
@ -85,6 +94,13 @@ public interface ITimestampedValuesClient : IDisposable
/// <param name="token"></param> /// <param name="token"></param>
Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token); Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token);
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="idDiscriminator"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> GetLast<T>(Guid idDiscriminator, int take, CancellationToken token);
} }

View File

@ -1,4 +1,4 @@
using DD.Persistence.Models.ChangeLog; using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
using Refit; using Refit;
@ -16,13 +16,13 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
/// Импорт с заменой: удаление старых строк и добавление новых /// Импорт с заменой: удаление старых строк и добавление новых
/// </summary> /// </summary>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")] [Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<IDictionary<string, object>> dtos, string comment, CancellationToken token); Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией) /// Получение актуальных данных на определенную дату (с пагинацией)
/// </summary> /// </summary>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")] [Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<ChangeLogBaseDto>>> GetByDate( Task<IApiResponse<PaginationContainer<ChangeLogValuesDto>>> GetByDate(
Guid idDiscriminator, Guid idDiscriminator,
DateTimeOffset moment, DateTimeOffset moment,
[Query] PaginationRequest paginationRequest, [Query] PaginationRequest paginationRequest,
@ -35,26 +35,40 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить одну запись
/// </summary> /// </summary>
[Post($"{BaseRoute}/{{idDiscriminator}}")] [Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange( Task<IApiResponse<int>> Add(Guid idDiscriminator, ChangeLogValuesDto dto, CancellationToken token);
Guid idDiscriminator,
IEnumerable<IDictionary<string, object>> dtos, /// <summary>
string comment, /// Добавить несколько записей
CancellationToken token); /// </summary>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
/// </summary>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(ChangeLogValuesDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить несколько записей
/// </summary> /// </summary>
[Put($"{BaseRoute}")] [Put($"{BaseRoute}/range")]
Task<IApiResponse<int>> UpdateRange(Guid idDiscriminator, IEnumerable<ChangeLogBaseDto> dtos, string comment, CancellationToken token); Task<IApiResponse<int>> UpdateRange(IEnumerable<ChangeLogValuesDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
/// </summary>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Удалить несколько записей
/// </summary> /// </summary>
[Delete($"{BaseRoute}")] [Delete($"{BaseRoute}/range")]
Task<IApiResponse<int>> DeleteRange(Guid idDiscriminator, [Body] IEnumerable<Guid> ids, string comment, CancellationToken token); Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids, CancellationToken token);
/// <summary> /// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени) /// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
@ -62,21 +76,4 @@ public interface IRefitChangeLogClient : IRefitClient, IDisposable
[Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")] [Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator, CancellationToken token); Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получение статистики журнала изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[Get($"{BaseRoute}/statistics")]
Task<IApiResponse<IEnumerable<ChangeLogStatisticsDto>>> GetStatistics(ChangeLogQuery request, CancellationToken token);
/// <summary>
/// Получение истории по журналу изменений
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
[Get($"{BaseRoute}/history")]
Task<IApiResponse<IEnumerable<ChangeLogCommitDto>>> GetHistory(ChangeLogQuery request, CancellationToken token);
} }

View File

@ -23,7 +23,6 @@ public interface IRefitTimestampedValuesClient : IRefitClient, IDisposable
[Get($"{baseUrl}")] [Get($"{baseUrl}")]
Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> Get([Query(CollectionFormat.Multi)] IEnumerable<Guid> discriminatorIds, Task<IApiResponse<IEnumerable<TimestampedValuesDto>>> Get([Query(CollectionFormat.Multi)] IEnumerable<Guid> discriminatorIds,
DateTimeOffset? timestampBegin, DateTimeOffset? timestampBegin,
[Query] string? filterTree,
[Query(CollectionFormat.Multi)] IEnumerable<string>? columnNames, [Query(CollectionFormat.Multi)] IEnumerable<string>? columnNames,
int skip, int skip,
int take, int take,

View File

@ -1,6 +0,0 @@
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
internal interface IMapperStorage
{
TimestampedSetMapper GetMapper<T>(Guid idDiscriminator);
TimestampedSetMapper? GetMapper(Guid idDiscriminator);
}

View File

@ -1,6 +0,0 @@
using DD.Persistence.Client.Clients.Interfaces;
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
public interface ISetpointMappingClient : ISetpointClient
{
}

View File

@ -1,45 +0,0 @@
using DD.Persistence.Client.Clients.Interfaces;
namespace DD.Persistence.Client.Clients.Mapping.Abstractions;
/// <summary>
/// Маппинг - обертка для клиента по работе с данными
/// </summary>
public interface ITimestampedMappingClient : ITimestampedValuesClient
{
/// <summary>
/// Получить данные с преобразованием к заданному типу
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="geTimestamp"></param>
/// <param name="columnNames">Фильтр свойств набора</param>
/// <param name="filterTree"></param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
Task<IEnumerable<T>> GetMapped<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить набор данных, преобразованных к соответствующим типам из заданного конфига
/// </summary>
/// <param name="discriminatorIds"></param>
/// <param name="timestampBegin"></param>
/// <param name="filterTree"></param>
/// <param name="columnNames"></param>
/// <param name="skip"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IDictionary<Guid, IEnumerable<object>>> GetMultiMapped(IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
/// <summary>
/// Получить данные с конца с преобразованием к заданному типу
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="idDiscriminator"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<T>> GetLastMapped<T>(Guid idDiscriminator, int take, CancellationToken token);
}

View File

@ -1,103 +0,0 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Configurations;
using System.Text.Json;
namespace DD.Persistence.Client.Clients.Mapping.Clients;
/// <inheritdoc/>
public class SetpointMappingClient : ISetpointMappingClient
{
private readonly ISetpointClient setpointClient;
private readonly MappingConfig mappingConfigs;
/// <inheritdoc/>
public SetpointMappingClient(ISetpointClient setpointClient, MappingConfig mappingConfigs)
{
this.setpointClient = setpointClient;
this.mappingConfigs = mappingConfigs;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
=> (await setpointClient.GetCurrent(setpointKeys, token))
.Select(x => new SetpointValueDto
{
Key = x.Key,
Value = DeserializeValue(x.Key, (JsonElement)x.Value)
});
/// <inheritdoc/>
public async Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointConfigs, CancellationToken token)
{
var result = (await setpointClient.GetCurrent(setpointConfigs, token))
.ToDictionary(x => x.Key, x => DeserializeValue(x.Key, (JsonElement)x.Value));
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
{
var result = await setpointClient.GetHistory(setpointKeys, historyMoment, token);
foreach (var dto in result)
dto.Value = DeserializeValue(dto.Key, (JsonElement)dto.Value);
return result;
}
/// <inheritdoc/>
public async Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointClient.GetLog(setpointKeys, token);
foreach (var item in result)
DeserializeList(result[item.Key]);
return result;
}
/// <inheritdoc/>
public async Task<IEnumerable<SetpointLogDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token)
{
var result = await setpointClient.GetPart(dateBegin, take, token);
DeserializeList(result);
return result;
}
/// <inheritdoc/>
public async Task Add(Guid setpointKey, object newValue, CancellationToken token)
=> await setpointClient.Add(setpointKey, newValue, token);
/// <inheritdoc/>
public async Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token)
=> await setpointClient.GetDatesRangeAsync(token);
/// <inheritdoc/>
public void Dispose()
{
setpointClient.Dispose();
}
private object DeserializeValue(Guid key, JsonElement value)
{
if (mappingConfigs.TryGetValue(key, out var type))
return value.Deserialize(type)!;
return value;
}
private void DeserializeList(IEnumerable<SetpointLogDto>? result)
{
if (result is null)
return;
foreach (var log in result)
log.Value = DeserializeValue(log.Key, (JsonElement)log.Value);
}
}

View File

@ -1,107 +0,0 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using Microsoft.Extensions.Logging;
namespace DD.Persistence.Client.Clients.Mapping.Clients;
/// <inheritdoc/>
internal class TimestampedMappingClient : ITimestampedMappingClient
{
private readonly ITimestampedValuesClient client;
private readonly IMapperStorage mapperStorage;
public TimestampedMappingClient(ITimestampedValuesClient client, IMapperStorage mapperStorage)
{
this.client = client;
this.mapperStorage = mapperStorage;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetMapped<T>(Guid discriminatorId, DateTimeOffset? geTimestamp,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get([discriminatorId], geTimestamp, filterTree, columnNames, skip, take, token);
var mapper = mapperStorage.GetMapper<T>(discriminatorId);
var mappedDtos = data.Select(mapper.DeserializeTimeStampedData).OfType<T>();
return mappedDtos;
}
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetLastMapped<T>(Guid idDiscriminator, int take, CancellationToken token)
{
var data = await GetLast(idDiscriminator, take, token);
var mapper = mapperStorage.GetMapper<T>(idDiscriminator);
var mappedDtos = data.Select(mapper.DeserializeTimeStampedData).OfType<T>();
return mappedDtos;
}
/// <inheritdoc/>
public async Task<IDictionary<Guid, IEnumerable<object>>> GetMultiMapped(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await client.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token);
var result = discriminatorIds
.ToDictionary(discriminatorId => discriminatorId, discriminatorId =>
{
var mapper = mapperStorage.GetMapper(discriminatorId);
ArgumentNullException.ThrowIfNull(mapper);
var mappedDtos = data
.Where(e => e.DiscriminatorId == discriminatorId)
.Select(mapper.DeserializeTimeStampedData);
return mappedDtos;
});
return result;
}
/// <inheritdoc/>
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
=> await client.AddRange(discriminatorId, dtos, token);
/// <inheritdoc/>
public async Task<int> Count(Guid discriminatorId, CancellationToken token)
=> await client.Count(discriminatorId, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? timestampBegin,
string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
=> await client.Get(discriminatorIds, timestampBegin, filterTree, columnNames, skip, take, token);
/// <inheritdoc/>
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
=> await client.GetDatesRange(discriminatorId, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetFirst(Guid discriminatorId, int take, CancellationToken token)
=> await client.GetFirst(discriminatorId, take, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
=> await client.GetGtDate(discriminatorId, timestampBegin, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetLast(Guid discriminatorId, int take, CancellationToken token)
=> await client.GetLast(discriminatorId, take, token);
/// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetResampledData(Guid discriminatorId, DateTimeOffset timestampBegin, double intervalSec = 600, int approxPointsCount = 1024, CancellationToken token = default)
=> await client.GetResampledData(discriminatorId, timestampBegin, intervalSec, approxPointsCount, token);
/// <inheritdoc/>
public void Dispose()
{
client.Dispose();
}
}

View File

@ -1,33 +0,0 @@
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
namespace DD.Persistence.Client.Clients.Mapping;
internal class MapperStorage : IMapperStorage
{
private readonly ConcurrentDictionary<Guid, TimestampedSetMapper> mapperCache = new();
private readonly MappingConfig mappingConfigs;
private readonly ILogger<TimestampedSetMapper> logger;
public MapperStorage(MappingConfig mappingConfigs, ILogger<TimestampedSetMapper> logger)
{
this.mappingConfigs = mappingConfigs;
this.logger = logger;
}
public TimestampedSetMapper? GetMapper(Guid idDiscriminator)
{
if (mappingConfigs.TryGetValue(idDiscriminator, out var type))
return mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper(idDiscriminator, type, logger));
return null;
}
public TimestampedSetMapper GetMapper<T>(Guid idDiscriminator)
{
return mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper(idDiscriminator, typeof(T), logger));
}
}

View File

@ -3,6 +3,9 @@ using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models; using DD.Persistence.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
namespace DD.Persistence.Client.Clients; namespace DD.Persistence.Client.Clients;
@ -10,12 +13,15 @@ namespace DD.Persistence.Client.Clients;
public class SetpointClient : BaseClient, ISetpointClient public class SetpointClient : BaseClient, ISetpointClient
{ {
private readonly IRefitSetpointClient refitSetpointClient; private readonly IRefitSetpointClient refitSetpointClient;
private readonly ISetpointConfigStorage setpointConfigStorage;
public SetpointClient( public SetpointClient(
IRefitClientFactory<IRefitSetpointClient> refitSetpointClientFactory, IRefitClientFactory<IRefitSetpointClient> refitSetpointClientFactory,
ISetpointConfigStorage setpointConfigStorage,
ILogger<SetpointClient> logger) : base(logger) ILogger<SetpointClient> logger) : base(logger)
{ {
this.refitSetpointClient = refitSetpointClientFactory.Create(); this.refitSetpointClient = refitSetpointClientFactory.Create();
this.setpointConfigStorage = setpointConfigStorage;
} }
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token) public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
@ -25,7 +31,7 @@ public class SetpointClient : BaseClient, ISetpointClient
return result!.Select(x => new SetpointValueDto { return result!.Select(x => new SetpointValueDto {
Key = x.Key, Key = x.Key,
Value = x.Value Value = DeserializeValue(x.Key, x.Value)
}); });
} }
@ -37,7 +43,7 @@ public class SetpointClient : BaseClient, ISetpointClient
async () => await refitSetpointClient.GetCurrent(setpointConfigs, token), token); async () => await refitSetpointClient.GetCurrent(setpointConfigs, token), token);
return result!.ToDictionary(x => x.Key,x => (object)x.Value); return result!.ToDictionary(x => x.Key,x => DeserializeValue(x.Key,x.Value));
} }
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token) public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
@ -45,6 +51,9 @@ public class SetpointClient : BaseClient, ISetpointClient
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetHistory(setpointKeys, historyMoment, token), token); async () => await refitSetpointClient.GetHistory(setpointKeys, historyMoment, token), token);
foreach(var dto in result)
dto.Value = DeserializeValue(dto.Key, (JsonElement)dto.Value);
return result!; return result!;
} }
@ -54,6 +63,9 @@ public class SetpointClient : BaseClient, ISetpointClient
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetLog(setpointKeys, token), token); async () => await refitSetpointClient.GetLog(setpointKeys, token), token);
foreach(var item in result)
DeserializeList(result[item.Key]);
return result!; return result!;
} }
@ -70,6 +82,8 @@ public class SetpointClient : BaseClient, ISetpointClient
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitSetpointClient.GetPart(dateBegin, take, token), token); async () => await refitSetpointClient.GetPart(dateBegin, take, token), token);
DeserializeList(result);
return result!; return result!;
} }
@ -87,4 +101,21 @@ public class SetpointClient : BaseClient, ISetpointClient
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private object DeserializeValue(Guid key, JsonElement value)
{
if (setpointConfigStorage.TryGetType(key, out var type))
return value.Deserialize(type)!;
return value;
}
private void DeserializeList(IEnumerable<SetpointLogDto>? result)
{
foreach (var log in result)
log.Value = DeserializeValue(log.Key, (JsonElement)log.Value);
}
} }

View File

@ -1,4 +1,4 @@
using DD.Persistence.Client.Clients.Base; using DD.Persistence.Client.Clients.Base;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Models; using DD.Persistence.Models;
@ -19,7 +19,8 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
this.refitTimestampedSetClient = refitTimestampedSetClientFactory.Create(); this.refitTimestampedSetClient = refitTimestampedSetClientFactory.Create();
} }
/// <inheritdoc/>
private readonly ConcurrentDictionary<Guid, TimestampedSetMapperBase> mapperCache = new();
/// <inheritdoc/> /// <inheritdoc/>
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> sets, CancellationToken token) public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> sets, CancellationToken token)
@ -31,13 +32,22 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, string? filterTree, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token) public async Task<IEnumerable<TimestampedValuesDto>> Get(IEnumerable<Guid> discriminatorIds, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{ {
var result = await ExecuteGetResponse( var result = await ExecuteGetResponse(
async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, filterTree, columnNames, skip, take, token), token); async () => await refitTimestampedSetClient.Get(discriminatorIds, geTimestamp, columnNames, skip, take, token), token);
return result!; return result!;
} }
/// <inheritdoc/>
public async Task<IEnumerable<T>> Get<T>(Guid discriminatorId, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
{
var data = await Get([discriminatorId], geTimestamp, columnNames, skip, take, token);
var mapper = GetMapper<T>(discriminatorId);
return data.Select(mapper.DeserializeTimeStampedData);
}
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token) public async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{ {
@ -92,7 +102,20 @@ public class TimestampedValuesClient : BaseClient, ITimestampedValuesClient
return result; return result;
} }
/// <inheritdoc/>
public async Task<IEnumerable<T>> GetLast<T>(Guid idDiscriminator, int take, CancellationToken token)
{
var data = await GetLast(idDiscriminator, take, token);
var mapper = GetMapper<T>(idDiscriminator);
return data.Select(mapper.DeserializeTimeStampedData);
}
/// <inheritdoc/>
private TimestampedSetMapper<T> GetMapper<T>(Guid idDiscriminator)
{
return (TimestampedSetMapper<T>)mapperCache.GetOrAdd(idDiscriminator, name => new TimestampedSetMapper<T>(idDiscriminator));
}
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Client.CustomExceptions;
/// <summary>
/// Ошибка, которая выбрасывается в случае отсутствия необходимого свойства в appsettings.json
/// </summary>
public class AppSettingsPropertyNotFoundException : Exception
{
/// <summary>
///
/// </summary>
/// <param name="message"></param>
public AppSettingsPropertyNotFoundException(string message)
: base(message) { }
}

View File

@ -52,7 +52,6 @@
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.0" />
<PackageReference Include="Refit" Version="8.0.0" /> <PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" /> <PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,10 +1,6 @@
using DD.Persistence.Client.Clients; using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping;
using DD.Persistence.Client.Clients.Mapping.Abstractions;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace DD.Persistence.Client; namespace DD.Persistence.Client;
@ -19,7 +15,7 @@ public static class DependencyInjection
/// </summary> /// </summary>
/// <param name="services"></param> /// <param name="services"></param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection AddPersistenceClients(this IServiceCollection services) public static IServiceCollection AddPersistenceClients(this IServiceCollection services, Dictionary<Guid, Type>? setpointTypeConfigs = null)
{ {
services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>)); services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>));
services.AddTransient<IChangeLogClient, ChangeLogClient>(); services.AddTransient<IChangeLogClient, ChangeLogClient>();
@ -29,17 +25,10 @@ public static class DependencyInjection
services.AddTransient<ITimestampedValuesClient, TimestampedValuesClient>(); services.AddTransient<ITimestampedValuesClient, TimestampedValuesClient>();
services.AddTransient<IWitsDataClient, WitsDataClient>(); services.AddTransient<IWitsDataClient, WitsDataClient>();
return services; services.AddSingleton<ISetpointConfigStorage, SetpointConfigStorage>(provider =>
} {
return new SetpointConfigStorage(setpointTypeConfigs);
});
public static IServiceCollection AddPersistenceMapping(this IServiceCollection services, MappingConfig mappingConfigs)
{
services.AddSingleton(mappingConfigs);
services.AddSingleton<IMapperStorage, MapperStorage>();
services.AddTransient<ISetpointMappingClient, SetpointMappingClient>();
services.AddTransient<ITimestampedMappingClient, TimestampedMappingClient>();
return services; return services;
} }
} }

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Client;
public interface ISetpointConfigStorage
{
bool TryGetType(Guid id, out Type type);
}

View File

@ -1,9 +1,8 @@
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Client.Helpers; using DD.Persistence.Client.CustomExceptions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Refit; using Refit;
using System.Configuration;
using System.Text.Json; using System.Text.Json;
namespace DD.Persistence.Client; namespace DD.Persistence.Client;
@ -23,10 +22,10 @@ public class RefitClientFactory<T> : IRefitClientFactory<T> where T : IRefitClie
//this.client = factory.CreateClient(); //this.client = factory.CreateClient();
this.client = client; this.client = client;
var baseUrl = configuration.GetSection("ClientUrl").Get<string>(); var baseUrl = configuration.GetSection("PeristenceClientUrl").Get<string>();
if (String.IsNullOrEmpty(baseUrl)) if (String.IsNullOrEmpty(baseUrl))
{ {
var exception = new SettingsPropertyNotFoundException("В настройках конфигурации не указан адрес Persistence сервиса."); var exception = new AppSettingsPropertyNotFoundException("В настройках конфигурации не указан адрес Persistence сервиса.");
logger.LogError(exception.Message); logger.LogError(exception.Message);

View File

@ -0,0 +1,20 @@
namespace DD.Persistence.Client;
internal class SetpointConfigStorage : ISetpointConfigStorage
{
private readonly Dictionary<Guid, Type> setpointTypeConfigs;
public SetpointConfigStorage(Dictionary<Guid, Type>? setpointTypeConfigs)
{
this.setpointTypeConfigs = setpointTypeConfigs?? new Dictionary<Guid, Type>();
}
public bool TryGetType(Guid id, out Type type)
{
return setpointTypeConfigs.TryGetValue(id, out type);
}
public void AddOrReplace(Guid id, Type type)
{
setpointTypeConfigs[id] = type;
}
}

View File

@ -1,62 +1,65 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
namespace DD.Persistence.Client.Clients.Mapping; namespace DD.Persistence.Client;
internal class TimestampedSetMapper internal abstract class TimestampedSetMapperBase
{ {
private readonly Type entityType; public abstract object Map(TimestampedValuesDto data);
private readonly ILogger<TimestampedSetMapper> logger;
}
internal class TimestampedSetMapper<T> : TimestampedSetMapperBase
{
private readonly Type entityType = typeof(T);
public Guid IdDiscriminator { get; } public Guid IdDiscriminator { get; }
private readonly ConcurrentDictionary<string, PropertyInfo?> PropertyCache = new(); private readonly ConcurrentDictionary<string, PropertyInfo?> PropertyCache = new();
public TimestampedSetMapper(Guid idDiscriminator, Type entityType, ILogger<TimestampedSetMapper> logger) public TimestampedSetMapper(Guid idDiscriminator)
{ {
IdDiscriminator = idDiscriminator; IdDiscriminator = idDiscriminator;
this.entityType = entityType;
this.logger = logger;
} }
public object DeserializeTimeStampedData(TimestampedValuesDto data) public override object Map(TimestampedValuesDto data)
{ {
return DeserializeTimeStampedData(data)!;
}
public T DeserializeTimeStampedData(TimestampedValuesDto data)
{
if (entityType.IsValueType) if (entityType.IsValueType)
return MapStruct(data); return MapStruct(data);
else
return MapClass(data); return MapClass(data);
} }
private object MapClass(TimestampedValuesDto data) private T MapClass(TimestampedValuesDto data)
{ {
var entity = RuntimeHelpers.GetUninitializedObject(entityType); var entity = (T)RuntimeHelpers.GetUninitializedObject(typeof(T));
foreach (var (propertyName, value) in data.Values) foreach (var (propertyName, value) in data.Values)
{ {
if (value is JsonElement jsonElement) if (value is JsonElement jsonElement)
SetPropertyValueFromJson(ref entity, propertyName, jsonElement); SetPropertyValueFromJson(ref entity, propertyName, jsonElement);
} }
SetPropertyValue(ref entity, nameof(TimestampedValuesDto.Timestamp), data.Timestamp); SetPropertyValue(ref entity, "Timestamp", data.Timestamp);
SetPropertyValue(ref entity, nameof(TimestampedValuesDto.DiscriminatorId), data.DiscriminatorId);
return entity; return entity;
} }
private object MapStruct(TimestampedValuesDto data) private T MapStruct(TimestampedValuesDto data)
{ {
var entity = Activator.CreateInstance(entityType); var entity = Activator.CreateInstance<T>();
object boxedEntity = entity!; object boxedEntity = entity!;
foreach (var (propertyName, value) in data.Values) foreach (var (propertyName, value) in data.Values)
{ {
if (value is JsonElement jsonElement) if (value is JsonElement jsonElement)
SetPropertyValueForStructFromJson(ref boxedEntity, propertyName, jsonElement); SetPropertyValueForStructFromJson(ref boxedEntity, propertyName, jsonElement);
} }
SetPropertyValueForStruct(ref boxedEntity, nameof(TimestampedValuesDto.Timestamp), data.Timestamp); SetPropertyValueForStruct(ref boxedEntity, "Timestamp", data.Timestamp);
SetPropertyValueForStruct(ref boxedEntity, nameof(TimestampedValuesDto.DiscriminatorId), data.DiscriminatorId);
return boxedEntity; return (T)boxedEntity;
} }
private void SetPropertyValueForStructFromJson(ref object entity, string propertyName, JsonElement element) private void SetPropertyValueForStructFromJson(ref object entity, string propertyName, JsonElement element)
@ -72,7 +75,6 @@ internal class TimestampedSetMapper
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex.Message);
} }
} }
private void SetPropertyValueForStruct(ref object entity, string propertyName, object value) private void SetPropertyValueForStruct(ref object entity, string propertyName, object value)
@ -88,12 +90,11 @@ internal class TimestampedSetMapper
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex.Message);
} }
} }
private void SetPropertyValueFromJson(ref object entity, string propertyName, JsonElement jsonElement) private void SetPropertyValueFromJson(ref T entity, string propertyName, JsonElement jsonElement)
{ {
var property = GetPropertyInfo(propertyName); var property = GetPropertyInfo(propertyName);
@ -107,11 +108,11 @@ internal class TimestampedSetMapper
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex.Message);
} }
} }
private void SetPropertyValue(ref object entity, string propertyName, object value) private void SetPropertyValue(ref T entity, string propertyName, object value)
{ {
var property = GetPropertyInfo(propertyName); var property = GetPropertyInfo(propertyName);
if (property is null) if (property is null)
@ -124,7 +125,6 @@ internal class TimestampedSetMapper
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex.Message);
} }
} }

View File

@ -1,13 +1,6 @@
using DD.Persistence.Database.Entity; using Microsoft.EntityFrameworkCore;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DD.Persistence.Database.Model; namespace DD.Persistence.Database.Model;

View File

@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace DD.Persistence.Database.Postgres.Migrations namespace DD.Persistence.Database.Postgres.Migrations
{ {
[DbContext(typeof(PersistencePostgresContext))] [DbContext(typeof(PersistencePostgresContext))]
[Migration("20250228042803_Innit")] [Migration("20250203061429_Init")]
partial class Innit partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -33,17 +33,29 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Ключ записи"); .HasComment("Ключ записи");
b.Property<Guid>("IdCreatedCommit") b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id коммита на создание записи"); .HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext") b.Property<Guid?>("IdNext")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id заменяющей записи"); .HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit") b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("uuid") .HasColumnType("timestamp with time zone")
.HasComment("Id коммита на устаревание записи"); .HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
@ -52,40 +64,24 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdCreatedCommit");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log"); b.ToTable("change_log");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id коммита"); .HasComment("Идентификатор схемы данных");
b.Property<string>("Comment") b.Property<string>("PropNames")
.IsRequired() .IsRequired()
.HasColumnType("text") .HasColumnType("jsonb")
.HasComment("Комментарий к коммиту"); .HasComment("Наименования полей в порядке индексации");
b.Property<DateTimeOffset>("Creation") b.HasKey("DiscriminatorId");
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("DiscriminatorId") b.ToTable("data_scheme");
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -133,30 +129,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("parameter_data"); b.ToTable("parameter_data");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b => modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
{ {
b.Property<Guid>("Key") b.Property<Guid>("Key")
@ -235,23 +207,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.ToTable("timestamped_values"); b.ToTable("timestamped_values");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
{ {
b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System") b.HasOne("DD.Persistence.Database.Entity.DataSourceSystem", "System")
@ -263,11 +218,15 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{ {
b.Navigation("ChangeLogCreatedItems"); b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChangeLogObsoletedItems"); b.Navigation("DataScheme");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -7,24 +7,39 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace DD.Persistence.Database.Postgres.Migrations namespace DD.Persistence.Database.Postgres.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class Innit : Migration public partial class Init : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "change_log_commit", name: "change_log",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита"), Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Пользователь, создавший коммит"), IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"),
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор таблицы"), IdAuthor = table.Column<Guid>(type: "uuid", nullable: false, comment: "Автор изменения"),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания коммита"), IdEditor = table.Column<Guid>(type: "uuid", nullable: true, comment: "Редактор"),
Comment = table.Column<string>(type: "text", nullable: false, comment: "Комментарий к коммиту") Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата создания записи"),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true, comment: "Дата устаревания (например при удалении)"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение")
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_change_log_commit", x => x.Id); table.PrimaryKey("PK_change_log", x => x.Id);
});
migrationBuilder.CreateTable(
name: "data_scheme",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
PropNames = table.Column<string>(type: "jsonb", nullable: false, comment: "Наименования полей в порядке индексации")
},
constraints: table =>
{
table.PrimaryKey("PK_data_scheme", x => x.DiscriminatorId);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -54,20 +69,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp }); table.PrimaryKey("PK_parameter_data", x => new { x.DiscriminatorId, x.ParameterId, x.Timestamp });
}); });
migrationBuilder.CreateTable(
name: "scheme_property",
columns: table => new
{
DiscriminatorId = table.Column<Guid>(type: "uuid", nullable: false, comment: "Идентификатор схемы данных"),
Index = table.Column<int>(type: "integer", nullable: false, comment: "Индекс поля"),
PropertyName = table.Column<string>(type: "text", nullable: false, comment: "Наименования индексируемого поля"),
PropertyKind = table.Column<byte>(type: "smallint", nullable: false, comment: "Тип индексируемого поля")
},
constraints: table =>
{
table.PrimaryKey("PK_scheme_property", x => new { x.DiscriminatorId, x.Index });
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "setpoint", name: "setpoint",
columns: table => new columns: table => new
@ -93,32 +94,12 @@ namespace DD.Persistence.Database.Postgres.Migrations
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp }); table.PrimaryKey("PK_timestamped_values", x => new { x.DiscriminatorId, x.Timestamp });
});
migrationBuilder.CreateTable(
name: "change_log",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ записи"),
IdNext = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id заменяющей записи"),
Value = table.Column<string>(type: "jsonb", nullable: false, comment: "Значение"),
IdCreatedCommit = table.Column<Guid>(type: "uuid", nullable: false, comment: "Id коммита на создание записи"),
IdObsoletedCommit = table.Column<Guid>(type: "uuid", nullable: true, comment: "Id коммита на устаревание записи")
},
constraints: table =>
{
table.PrimaryKey("PK_change_log", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_change_log_change_log_commit_IdCreatedCommit", name: "FK_timestamped_values_data_scheme_DiscriminatorId",
column: x => x.IdCreatedCommit, column: x => x.DiscriminatorId,
principalTable: "change_log_commit", principalTable: "data_scheme",
principalColumn: "Id", principalColumn: "DiscriminatorId",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_change_log_change_log_commit_IdObsoletedCommit",
column: x => x.IdObsoletedCommit,
principalTable: "change_log_commit",
principalColumn: "Id");
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -143,16 +124,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_change_log_IdCreatedCommit",
table: "change_log",
column: "IdCreatedCommit");
migrationBuilder.CreateIndex(
name: "IX_change_log_IdObsoletedCommit",
table: "change_log",
column: "IdObsoletedCommit");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_tech_message_SystemId", name: "IX_tech_message_SystemId",
table: "tech_message", table: "tech_message",
@ -168,9 +139,6 @@ namespace DD.Persistence.Database.Postgres.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "parameter_data"); name: "parameter_data");
migrationBuilder.DropTable(
name: "scheme_property");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "setpoint"); name: "setpoint");
@ -181,10 +149,10 @@ namespace DD.Persistence.Database.Postgres.Migrations
name: "timestamped_values"); name: "timestamped_values");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "change_log_commit"); name: "data_source_system");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "data_source_system"); name: "data_scheme");
} }
} }
} }

View File

@ -30,17 +30,29 @@ namespace DD.Persistence.Database.Postgres.Migrations
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Ключ записи"); .HasComment("Ключ записи");
b.Property<Guid>("IdCreatedCommit") b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания записи");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id коммита на создание записи"); .HasComment("Автор изменения");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasComment("Редактор");
b.Property<Guid?>("IdNext") b.Property<Guid?>("IdNext")
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id заменяющей записи"); .HasComment("Id заменяющей записи");
b.Property<Guid?>("IdObsoletedCommit") b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("uuid") .HasColumnType("timestamp with time zone")
.HasComment("Id коммита на устаревание записи"); .HasComment("Дата устаревания (например при удалении)");
b.Property<string>("Value") b.Property<string>("Value")
.IsRequired() .IsRequired()
@ -49,40 +61,24 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("IdCreatedCommit"); b.ToTable("change_log");
b.HasIndex("IdObsoletedCommit");
b.ToTable("change_log", (string)null);
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataScheme", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("DiscriminatorId")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid") .HasColumnType("uuid")
.HasComment("Id коммита"); .HasComment("Идентификатор схемы данных");
b.Property<string>("Comment") b.Property<string>("PropNames")
.IsRequired() .IsRequired()
.HasColumnType("text") .HasColumnType("jsonb")
.HasComment("Комментарий к коммиту"); .HasComment("Наименования полей в порядке индексации");
b.Property<DateTimeOffset>("Creation") b.HasKey("DiscriminatorId");
.HasColumnType("timestamp with time zone")
.HasComment("Дата создания коммита");
b.Property<Guid>("DiscriminatorId") b.ToTable("data_scheme");
.HasColumnType("uuid")
.HasComment("Дискриминатор таблицы");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasComment("Пользователь, создавший коммит");
b.HasKey("Id");
b.ToTable("change_log_commit", (string)null);
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b => modelBuilder.Entity("DD.Persistence.Database.Entity.DataSourceSystem", b =>
@ -103,7 +99,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("SystemId"); b.HasKey("SystemId");
b.ToTable("data_source_system", (string)null); b.ToTable("data_source_system");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ParameterData", b => modelBuilder.Entity("DD.Persistence.Database.Entity.ParameterData", b =>
@ -127,31 +123,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("DiscriminatorId", "ParameterId", "Timestamp"); b.HasKey("DiscriminatorId", "ParameterId", "Timestamp");
b.ToTable("parameter_data", (string)null); b.ToTable("parameter_data");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.SchemeProperty", b =>
{
b.Property<Guid>("DiscriminatorId")
.HasColumnType("uuid")
.HasComment("Идентификатор схемы данных");
b.Property<int>("Index")
.HasColumnType("integer")
.HasComment("Индекс поля");
b.Property<byte>("PropertyKind")
.HasColumnType("smallint")
.HasComment("Тип индексируемого поля");
b.Property<string>("PropertyName")
.IsRequired()
.HasColumnType("text")
.HasComment("Наименования индексируемого поля");
b.HasKey("DiscriminatorId", "Index");
b.ToTable("scheme_property", (string)null);
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b => modelBuilder.Entity("DD.Persistence.Database.Entity.Setpoint", b =>
@ -174,7 +146,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("Key", "Timestamp"); b.HasKey("Key", "Timestamp");
b.ToTable("setpoint", (string)null); b.ToTable("setpoint");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
@ -209,7 +181,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasIndex("SystemId"); b.HasIndex("SystemId");
b.ToTable("tech_message", (string)null); b.ToTable("tech_message");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
@ -229,24 +201,7 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.HasKey("DiscriminatorId", "Timestamp"); b.HasKey("DiscriminatorId", "Timestamp");
b.ToTable("timestamped_values", (string)null); b.ToTable("timestamped_values");
});
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLog", b =>
{
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "CreatedCommit")
.WithMany("ChangeLogCreatedItems")
.HasForeignKey("IdCreatedCommit")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DD.Persistence.Database.Entity.ChangeLogCommit", "ObsoletedCommit")
.WithMany("ChangeLogObsoletedItems")
.HasForeignKey("IdObsoletedCommit");
b.Navigation("CreatedCommit");
b.Navigation("ObsoletedCommit");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TechMessage", b =>
@ -260,11 +215,15 @@ namespace DD.Persistence.Database.Postgres.Migrations
b.Navigation("System"); b.Navigation("System");
}); });
modelBuilder.Entity("DD.Persistence.Database.Entity.ChangeLogCommit", b => modelBuilder.Entity("DD.Persistence.Database.Entity.TimestampedValues", b =>
{ {
b.Navigation("ChangeLogCreatedItems"); b.HasOne("DD.Persistence.Database.Entity.DataScheme", "DataScheme")
.WithMany()
.HasForeignKey("DiscriminatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ChangeLogObsoletedItems"); b.Navigation("DataScheme");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
@ -7,14 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,4 +1,6 @@
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.ModelsAbstractions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@ -9,32 +11,29 @@ namespace DD.Persistence.Database.Entity;
/// Часть записи, описывающая изменение /// Часть записи, описывающая изменение
/// </summary> /// </summary>
[Table("change_log")] [Table("change_log")]
public class ChangeLog public class ChangeLog : IChangeLog
{ {
[Key, Comment("Ключ записи")] [Key, Comment("Ключ записи")]
public Guid Id { get; set; } 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 заменяющей записи")] [Comment("Id заменяющей записи")]
public Guid? IdNext { get; set; } public Guid? IdNext { get; set; }
[Column(TypeName = "jsonb"), Comment("Значение")] [Column(TypeName = "jsonb"), Comment("Значение")]
public required IDictionary<string, object> Value { get; set; } public required IDictionary<string, object> Value { get; set; }
[Required, Comment("Id коммита на создание записи")]
public Guid IdCreatedCommit { get; set; }
[Comment("Id коммита на устаревание записи")]
public Guid? IdObsoletedCommit { get; set; }
/// <summary>
/// коммит для актуальной записи
/// </summary>
[Required, ForeignKey(nameof(IdCreatedCommit)), Comment("Коммит пользователя на создание записи")]
public virtual ChangeLogCommit CreatedCommit { get; set; } = null!;
/// <summary>
/// коммит для устаревшей записи
/// </summary>
[ForeignKey(nameof(IdObsoletedCommit)), Comment("Коммит пользователя на устаревание записи")]
public virtual ChangeLogCommit? ObsoletedCommit { get; set; }
} }

View File

@ -1,34 +0,0 @@
using DD.Persistence.Database.EntityAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
/// <summary>
/// Таблица c коммитами пользователей
/// </summary>
[Table("change_log_commit")]
public class ChangeLogCommit: IDiscriminatorItem
{
[Key, Comment("Id коммита")]
public Guid Id { get; set; }
[Comment("Пользователь, создавший коммит")]
public Guid IdAuthor { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid DiscriminatorId { get; set; }
[Comment("Дата создания коммита")]
public DateTimeOffset Creation { get; set; }
[Comment("Комментарий к коммиту")]
public required string Comment { get; set; }
[Required, InverseProperty(nameof(ChangeLog.CreatedCommit)), Comment("Записи, добавленные в журнал изменений")]
public virtual ICollection<ChangeLog> ChangeLogCreatedItems { get; set; } = null!;
[Required, InverseProperty(nameof(ChangeLog.ObsoletedCommit)), Comment("Устаревшие записи в журнале изменений")]
public virtual ICollection<ChangeLog> ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DD.Persistence.Database.Entity;
[Table("data_scheme")]
public class DataScheme
{
[Key, Comment("Идентификатор схемы данных"),]
public Guid DiscriminatorId { get; set; }
[Comment("Наименования полей в порядке индексации"), Column(TypeName = "jsonb")]
public string[] PropNames { get; set; } = [];
}

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("parameter_data")] [Table("parameter_data")]
[PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))] [PrimaryKey(nameof(DiscriminatorId), nameof(ParameterId), nameof(Timestamp))]
public class ParameterData : IDiscriminatorItem, ITimestampedItem public class ParameterData : ITimestampedItem
{ {
[Required, Comment("Дискриминатор системы")] [Required, Comment("Дискриминатор системы")]
public Guid DiscriminatorId { get; set; } public Guid DiscriminatorId { get; set; }

View File

@ -1,24 +0,0 @@
using DD.Persistence.Database.EntityAbstractions;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json;
namespace DD.Persistence.Database.Entity;
[Table("scheme_property")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Index))]
public class SchemeProperty : IDiscriminatorItem
{
[Comment("Идентификатор схемы данных")]
public Guid DiscriminatorId { get; set; }
[Comment("Индекс поля")]
public int Index { get; set; }
[Comment("Наименования индексируемого поля")]
public required string PropertyName { get; set; }
[Comment("Тип индексируемого поля")]
public required JsonValueKind PropertyKind { get; set; }
}

View File

@ -17,15 +17,15 @@ public class TechMessage : ITimestampedItem
[Comment("Дата возникновения")] [Comment("Дата возникновения")]
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
[Column(TypeName = "varchar(512)"), Comment("Текст сообщения")] [Column(TypeName = "varchar(512)"), Comment("Текст сообщения")]
public required string Text { get; set; } public required string Text { get; set; }
[Required, Comment("Id системы, к которой относится сообщение")] [Required, Comment("Id системы, к которой относится сообщение")]
public required Guid SystemId { get; set; } public required Guid SystemId { get; set; }
[Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")] [Required, ForeignKey(nameof(SystemId)), Comment("Система, к которой относится сообщение")]
public virtual required DataSourceSystem System { get; set; } public virtual required DataSourceSystem System { get; set; }
[Comment("Статус события")] [Comment("Статус события")]
public int EventState { get; set; } public int EventState { get; set; }
} }

View File

@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity;
[Table("timestamped_values")] [Table("timestamped_values")]
[PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))] [PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))]
public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesItem public class TimestampedValues : ITimestampedItem
{ {
[Comment("Временная отметка"), Key] [Comment("Временная отметка"), Key]
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
@ -17,4 +17,7 @@ public class TimestampedValues : IDiscriminatorItem, ITimestampedItem, IValuesIt
[Comment("Данные"), Column(TypeName = "jsonb")] [Comment("Данные"), Column(TypeName = "jsonb")]
public required object[] Values { get; set; } public required object[] Values { get; set; }
[Required, ForeignKey(nameof(DiscriminatorId)), Comment("Идентификаторы")]
public virtual DataScheme? DataScheme { get; set; }
} }

View File

@ -10,6 +10,16 @@ public interface IChangeLog
/// </summary> /// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public Guid? IdEditor { get; set; }
/// <summary> /// <summary>
/// Дата создания записи /// Дата создания записи
/// </summary> /// </summary>
@ -20,4 +30,18 @@ public interface IChangeLog
/// </summary> /// </summary>
public DateTimeOffset? Obsolete { get; set; } public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id заменяющей записи
/// </summary>
public Guid? IdNext { get; set; }
/// <summary>
/// Дискриминатор таблицы
/// </summary>
public Guid IdDiscriminator { get; set; }
/// <summary>
/// Значение
/// </summary>
public IDictionary<string, object> Value { get; set; }
} }

View File

@ -1,8 +0,0 @@
namespace DD.Persistence.Database.EntityAbstractions;
public interface IDiscriminatorItem
{
/// <summary>
/// Дискриминатор
/// </summary>
Guid DiscriminatorId { get; set; }
}

View File

@ -1,14 +0,0 @@
using DD.Persistence.Database.Entity;
namespace DD.Persistence.Database.EntityAbstractions;
/// <summary>
/// Сущность с данными, принадлежащими к определенной схеме
/// </summary>
public interface IValuesItem
{
/// <summary>
/// Значения
/// </summary>
object[] Values { get; set; }
}

View File

@ -1,38 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.Helpers;
using System.Linq.Expressions;
namespace DD.Persistence.Database.Postgres.Extensions;
public static class SpecificationExtensions
{
public static Expression<Func<T, bool>>? Or<T>(this ISpecification<T> spec, ISpecification<T> otherSpec)
{
var parameter = Expression.Parameter(typeof(T), "x");
var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter);
var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter);
Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null
? Expression.OrElse(exprSpec1, exprSpec2)
: exprSpec1 ?? exprSpec2;
if (orExpression is null)
return null;
var lambdaExpr = Expression.Lambda<Func<T, bool>>(orExpression, parameter);
return lambdaExpr;
}
public static Expression? CombineWhereExpressions<T>(IEnumerable<WhereExpressionInfo<T>> whereExpressions, ParameterExpression parameter)
{
Expression? newExpr = null;
foreach (var where in whereExpressions)
{
var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter);
newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr);
}
return newExpr;
}
}

View File

@ -1,116 +0,0 @@
using Ardalis.Specification;
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Database.Specifications.Operation;
using DD.Persistence.Database.Specifications.ValuesItem;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.Visitors;
using DD.Persistence.Models;
using System.Text.Json;
namespace DD.Persistence.Database.Postgres.Helpers;
public static class FilterBuilder
{
public static IQueryable<TEntity> ApplyFilter<TEntity>(this IQueryable<TEntity> query, DataSchemeDto dataSchemeDto, TNode root)
where TEntity : class, IValuesItem
{
var filterSpec = dataSchemeDto.BuildFilter<TEntity>(root);
if (filterSpec != null)
return query.WithSpecification(filterSpec);
return query;
}
private static ISpecification<TEntity>? BuildFilter<TEntity>(this DataSchemeDto dataSchemeDto, TNode root)
where TEntity : IValuesItem
{
var result = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(root);
return result;
}
private static ISpecification<TEntity>? BuildSpecificationByNextNode<TEntity>(this DataSchemeDto dataSchemeDto, TNode node)
where TEntity : IValuesItem
{
var visitor = new NodeVisitor<ISpecification<TEntity>?>(
dataSchemeDto.VertexProcessing<TEntity>,
dataSchemeDto.LeafProcessing<TEntity>
);
var result = node.AcceptVisitor(visitor);
return result;
}
private static ISpecification<TEntity>? VertexProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TVertex vertex)
where TEntity : IValuesItem
{
var leftSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Left);
var rigthSpecification = dataSchemeDto.BuildSpecificationByNextNode<TEntity>(vertex.Rigth);
if (leftSpecification is null)
return rigthSpecification;
if (rigthSpecification is null)
return leftSpecification;
ISpecification<TEntity>? result = null;
switch (vertex.Operation)
{
case OperationEnum.And:
result = new AndSpec<TEntity>(leftSpecification, rigthSpecification);
break;
case OperationEnum.Or:
result = new OrSpec<TEntity>(leftSpecification, rigthSpecification);
break;
}
return result;
}
private static ISpecification<TEntity>? LeafProcessing<TEntity>(this DataSchemeDto dataSchemeDto, TLeaf leaf)
where TEntity : IValuesItem
{
var schemeProperty = dataSchemeDto.FirstOrDefault(e => e.PropertyName.Equals(leaf.PropName));
if (schemeProperty is null)
throw new ArgumentException($"Свойство {leaf.PropName} не найдено в схеме данных");
ISpecification<TEntity>? result = null;
switch (schemeProperty.PropertyKind)
{
case JsonValueKind.String:
var stringValue = Convert.ToString(leaf.Value);
var stringSpecifications = StringSpecifications<TEntity>();
result = stringSpecifications[leaf.Operation](schemeProperty.Index, stringValue);
break;
case JsonValueKind.Number:
var doubleValue = Convert.ToDouble(leaf.Value);
var doubleSpecifications = DoubleSpecifications<TEntity>();
result = doubleSpecifications[leaf.Operation](schemeProperty.Index, doubleValue);
break;
}
return result;
}
private static Dictionary<OperationEnum, Func<int, string?, ISpecification<TEntity>>> StringSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, string? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, string? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, string? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, string? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, string? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, string? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
private static Dictionary<OperationEnum, Func<int, double?, ISpecification<TEntity>>> DoubleSpecifications<TEntity>()
where TEntity : IValuesItem => new()
{
{ OperationEnum.Equal, (int index, double? value) => new ValueEqualSpec<TEntity>(index, value) },
{ OperationEnum.NotEqual, (int index, double? value) => new ValueNotEqualSpec<TEntity>(index, value) },
{ OperationEnum.Greate, (int index, double? value) => new ValueGreateSpec<TEntity>(index, value) },
{ OperationEnum.GreateOrEqual, (int index, double? value) => new ValueGreateOrEqualSpec<TEntity>(index, value) },
{ OperationEnum.Less, (int index, double? value) => new ValueLessSpec<TEntity>(index, value) },
{ OperationEnum.LessOrEqual, (int index, double? value) => new ValueLessOrEqualSpec<TEntity>(index, value) }
};
}

View File

@ -1,20 +0,0 @@
using System.Linq.Expressions;
namespace DD.Persistence.Database.Helpers;
public class ParameterReplacerVisitor : ExpressionVisitor
{
private readonly Expression _newExpression;
private readonly ParameterExpression _oldParameter;
private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression)
{
_oldParameter = oldParameter;
_newExpression = newExpression;
}
internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression)
=> new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression);
protected override Expression VisitParameter(ParameterExpression p)
=> p == _oldParameter ? _newExpression : p;
}

View File

@ -1,28 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using Mapster;
namespace DD.Persistence.Database;
public static class MapsterSetup
{
public static void Configure()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogBaseDto()
{
Value = src.Value,
Id = src.Id
});
TypeAdapterConfig<KeyValuePair<Guid, SchemePropertyDto>, SchemeProperty>.NewConfig()
.Map(dest => dest.DiscriminatorId, src => src.Key)
.Map(dest => dest.Index, src => src.Value.Index)
.Map(dest => dest.PropertyKind, src => src.Value.PropertyKind)
.Map(dest => dest.PropertyName, src => src.Value.PropertyName);
}
}

View File

@ -10,14 +10,12 @@ public class PersistenceDbContext : DbContext
{ {
public DbSet<Setpoint> Setpoint => Set<Setpoint>(); public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<SchemeProperty> SchemeProperty => Set<SchemeProperty>(); public DbSet<DataScheme> DataSchemes => Set<DataScheme>();
public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>(); public DbSet<TimestampedValues> TimestampedValues => Set<TimestampedValues>();
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>(); public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<ChangeLogCommit> ChangeLogCommit => Set<ChangeLogCommit>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>(); public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>(); public DbSet<ParameterData> ParameterData => Set<ParameterData>();
@ -32,6 +30,10 @@ public class PersistenceDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<DataScheme>()
.Property(e => e.PropNames)
.HasJsonConversion();
modelBuilder.Entity<TimestampedValues>() modelBuilder.Entity<TimestampedValues>()
.Property(e => e.Values) .Property(e => e.Values)
.HasJsonConversion(); .HasJsonConversion();

View File

@ -1,171 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.Entity;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogCommitRepository : IChangeLogCommitRepository
{
private DbContext db;
public ChangeLogCommitRepository(DbContext db)
{
this.db = db;
}
public async Task<ChangeLogCommitDto> Add(ChangeLogCommitCreateRequest request, CancellationToken token)
{
var commit = new ChangeLogCommit()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
DiscriminatorId = request.DiscriminatorId,
IdAuthor = request.IdAuthor,
Comment = request.Comment,
};
db.Set<ChangeLogCommit>().Add(commit);
await db.SaveChangesAsync(token);
var commitDto = commit.Adapt<ChangeLogCommitDto>();
return commitDto;
}
public async Task<IEnumerable<ChangeLogCommitDto>> GetHistory(ChangeLogQuery request, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == request.DiscriminatorId);
if (request.UserId.HasValue)
query = query.Where(e => e.IdAuthor == request.UserId);
if(request.GeDate.HasValue)
query = query.Where(e => e.Creation >= request.GeDate);
if (request.LeDate.HasValue)
query = query.Where(e => e.Creation <= request.LeDate);
query = query
.Include(e => e.ChangeLogCreatedItems)
.Include(e => e.ChangeLogObsoletedItems);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(entity => new ChangeLogCommitDto()
{
ChangeLogCreatedItems = entity.ChangeLogCreatedItems.Select(c => c.Adapt<ChangeLogDto>()),
ChangeLogObsoletedItems = entity.ChangeLogObsoletedItems.Select(c => c.Adapt<ChangeLogDto>()),
Comment = entity.Comment,
Creation = entity.Creation,
DiscriminatorId = entity.DiscriminatorId,
IdAuthor = entity.IdAuthor,
Id = entity.Id,
});
return dtos;
}
public async Task<IEnumerable<ChangeLogStatisticsDto>> GetStatistics(Guid discriminatorId, Guid? userId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId);
if (userId.HasValue)
query = query.Where(e => e.IdAuthor == userId);
var queryStat = query
.GroupBy(e => e.Creation.Date)
.Select(group => new
{
Date = group.Key,
CommitCount = group.Count(),
CreatedChangeLogCount = group.Sum(e => e.ChangeLogCreatedItems.Count()),
ObsoletedChangeLogCount = group.Sum(e => e.ChangeLogObsoletedItems.Count()),
});
var entities = await queryStat.ToListAsync(token);
var dtos = entities.Select(e => new ChangeLogStatisticsDto
{
CommitCount = e.CommitCount,
Date = new DateOnly(e.Date.Year, e.Date.Month, e.Date.Day),
CreatedChangeLogCount = e.CreatedChangeLogCount,
ObsoletedCount = e.ObsoletedChangeLogCount,
});
return dtos;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId)
.GroupBy(e => true)
.Select(group => new
{
Min = group.Min(e => e.Creation),
Max = group.Max(e => e.Creation)
});
var values = await query.FirstOrDefaultAsync(token);
if (values is null)
{
return null;
}
return new DatesRangeDto
{
From = values.Min,
To = values.Max,
};
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid discriminatorId, CancellationToken token)
{
var query = db.Set<ChangeLogCommit>()
.Where(e => e.DiscriminatorId == discriminatorId)
.GroupBy(e => e.Creation.Date)
.Select(g => g.Key);
var entities = await query.ToArrayAsync(token);
var dates = entities
.Select(e => new DateOnly(e.Year, e.Month, e.Minute));
return dates;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var min = dateBegin.ToUniversalTime();
var max = dateEnd.ToUniversalTime();
var query = db.Set<ChangeLogCommit>()
.Where(c => c.DiscriminatorId == idDiscriminator)
.Where(c => c.Creation >= min && c.Creation <= max)
.Include(c => c.ChangeLogCreatedItems)
.Include(c => c.ChangeLogObsoletedItems);
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities
.SelectMany(c => new[] { c.ChangeLogCreatedItems, c.ChangeLogObsoletedItems })
.SelectMany(c => c)
.Where(c => c != null)
.Select(ConvertChangeLogToDto);
return dtos;
}
private ChangeLogDto ConvertChangeLogToDto(ChangeLog entity)
{
var dto = entity.Adapt<ChangeLogDto>();
dto.Creation = entity.CreatedCommit?.Creation;
dto.Obsolete = entity.ObsoletedCommit?.Creation;
dto.IdAuthor = entity.CreatedCommit?.IdAuthor;
dto.IdEditor = entity.ObsoletedCommit?.IdAuthor;
return dto;
}
}

View File

@ -1,193 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using UuidExtensions;
namespace DD.Persistence.Database.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(ChangeLogCommitDto commitDto, IEnumerable<IDictionary<string, object>> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
IdCreatedCommit = commitDto.Id,
Value = dto,
};
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<int> MarkAsDeleted(IEnumerable<Guid> ids, ChangeLogCommitDto commit, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
.Where(s => s.IdObsoletedCommit == 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);
foreach (var entity in entities)
{
entity.IdObsoletedCommit = commit.Id;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> MarkAsDeletedByDiscriminator(ChangeLogCommitDto commit, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.CreatedCommit.DiscriminatorId == commit.DiscriminatorId)
.Where(e => e.IdObsoletedCommit == null);
var entities = await query.ToArrayAsync(token);
foreach (var entity in entities)
{
entity.IdObsoletedCommit = commit.Id;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(ChangeLogCommitDto commit, IEnumerable<IDictionary<string, object>> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeletedByDiscriminator(commit, token);
result += await AddRange(commit, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(ChangeLogCommitDto commitDto, IEnumerable<ChangeLogBaseDto> 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);
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 = new ChangeLog()
{
Id = Uuid7.Guid(),
Value = dto.Value,
IdCreatedCommit = commitDto.Id,
};
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.IdObsoletedCommit = commitDto.Id;
}
var result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<ChangeLogBaseDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
PaginationRequest paginationRequest,
CancellationToken token)
{
var momentUtc = moment.ToUniversalTime();
var query = db.Set<ChangeLog>()
.Where(s => s.CreatedCommit.DiscriminatorId == idDiscriminator)
.Where(e => e.CreatedCommit.Creation <= momentUtc)
.Where(e => e.ObsoletedCommit == null || e.ObsoletedCommit.Creation >= momentUtc)
.AsNoTracking();
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}
public async Task<IEnumerable<ChangeLogBaseDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = db.Set<ChangeLog>()
.Where(e => e.CreatedCommit.DiscriminatorId == idDiscriminator)
.Where(e => e.CreatedCommit.Creation >= date || (e.IdObsoletedCommit != null && e.ObsoletedCommit!.Creation >= date));
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities.Select(Convert);
return dtos;
}
public async Task<IEnumerable<ChangeLogDto>> GetByUserIdAndDiscriminatorId(ChangeLogQuery request, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Include(c => c.CreatedCommit)
.Include(c => c.ObsoletedCommit)
.Where(c => c.CreatedCommit.DiscriminatorId == request.DiscriminatorId)
.Where(c => c.CreatedCommit.IdAuthor == request.UserId
|| (c.ObsoletedCommit != null && c.ObsoletedCommit!.IdAuthor == request.UserId));
var entities = await query
.AsNoTracking()
.ToArrayAsync(token);
var dtos = entities.Select(ConvertFull);
return dtos;
}
private ChangeLogBaseDto Convert(ChangeLog entity) => entity.Adapt<ChangeLogBaseDto>();
private ChangeLogDto ConvertFull(ChangeLog entity)
{
var dto = entity.Adapt<ChangeLogDto>();
dto.Creation = entity.CreatedCommit.Creation;
dto.Obsolete = entity.ObsoletedCommit?.Creation;
dto.IdAuthor = entity.CreatedCommit.IdAuthor;
dto.IdEditor = entity.ObsoletedCommit?.IdAuthor;
return dto;
}
}

View File

@ -1,43 +0,0 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Repositories;
public class SchemePropertyRepository : ISchemePropertyRepository
{
protected DbContext db;
public SchemePropertyRepository(DbContext db)
{
this.db = db;
}
protected IQueryable<SchemeProperty> GetQueryReadOnly() => db.Set<SchemeProperty>();
public virtual async Task AddRange(DataSchemeDto dataSchemeDto, CancellationToken token)
{
var entities = dataSchemeDto.Select(e =>
KeyValuePair.Create(dataSchemeDto.DiscriminatorId, e)
.Adapt<SchemeProperty>()
);
await db.Set<SchemeProperty>().AddRangeAsync(entities, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entities = await query.ToArrayAsync(token);
DataSchemeDto? result = null;
if (entities.Length != 0)
{
var properties = entities.Select(e => e.Adapt<SchemePropertyDto>()).ToArray();
result = new DataSchemeDto(dataSchemeId, properties);
}
return result;
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
namespace DD.Persistence.Database.Specifications.Operation;
public class AndSpec<TEntity> : Specification<TEntity>
{
public AndSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
if (first is null || second is null)
return;
ApplyCriteria(first);
ApplyCriteria(second);
}
private void ApplyCriteria(ISpecification<TEntity> specification)
{
foreach (var criteria in specification.WhereExpressions)
{
Query.Where(criteria.Filter);
}
}
}

View File

@ -1,15 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.Database.Specifications.Operation;
public class OrSpec<TEntity> : Specification<TEntity>
{
public OrSpec(ISpecification<TEntity> first, ISpecification<TEntity> second)
{
var orExpression = first.Or(second);
if (orExpression == null)
return;
Query.Where(orExpression);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация эквивалентности значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) == value);
}
public ValueEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) == value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) >= 0);
}
public ValueGreateOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) >= value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "больше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueGreateSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueGreateSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) > 0);
}
public ValueGreateSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) > value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше либо равно" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessOrEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessOrEqualSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) <= 0);
}
public ValueLessOrEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) <= value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация "меньше" для значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueLessSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueLessSpec(int index, string? value)
{
Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) < 0);
}
public ValueLessSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) < value);
}
}

View File

@ -1,22 +0,0 @@
using Ardalis.Specification;
using DD.Persistence.Database.EntityAbstractions;
namespace DD.Persistence.Database.Specifications.ValuesItem;
/// <summary>
/// Спецификация неравенства значений IValuesItem в соответствии с индексацией
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class ValueNotEqualSpec<TEntity> : Specification<TEntity>
where TEntity : IValuesItem
{
public ValueNotEqualSpec(int index, string? value)
{
Query.Where(e => Convert.ToString(e.Values[index]) != value);
}
public ValueNotEqualSpec(int index, double? value)
{
Query.Where(e => Convert.ToDouble(e.Values[index]) != value);
}
}

View File

@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; 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;
namespace DD.Persistence.IntegrationTests; namespace DD.Persistence.IntegrationTests;
@ -39,11 +40,12 @@ public static class ApiTokenHelper
var nameIdetifier = Guid.NewGuid().ToString(); var nameIdetifier = Guid.NewGuid().ToString();
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
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(), "067beb95-3b6d-7370-8000-1b9b336a6cdc") new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
}; };
var tokenDescriptor = new SecurityTokenDescriptor var tokenDescriptor = new SecurityTokenDescriptor

View File

@ -3,22 +3,19 @@ using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Models.ChangeLog; using DD.Persistence.Models;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using UuidExtensions;
using Xunit; using Xunit;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace DD.Persistence.IntegrationTests.Controllers; namespace DD.Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest public class ChangeLogControllerTest : BaseIntegrationTest
{ {
private readonly IChangeLogClient client; private readonly IChangeLogClient client;
private readonly PaginationRequest paginationRequest; private static readonly Random generatorRandomDigits = new();
private static readonly Random random = new();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory) public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
{ {
@ -28,36 +25,54 @@ public class ChangeLogControllerTest : BaseIntegrationTest
client = scope.ServiceProvider client = scope.ServiceProvider
.GetRequiredService<IChangeLogClient>(); .GetRequiredService<IChangeLogClient>();
paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = string.Empty,
};
} }
[Fact] [Fact]
public async Task ClearAndInsertRange() public async Task ClearAndInsertRange_InEmptyDb()
{
// arrange
dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
// assert
Assert.Equal(2, result);
}
[Fact]
public async Task ClearAndInsertRange_InNotEmptyDb()
{ {
// arrange // arrange
var insertedCount = 10; var insertedCount = 10;
var idDiscriminator = Guid.NewGuid(); var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var otherOldDtos = GenerateChangeLogCreateRequest(10); var idDiscriminator = createdResult.Item1;
await client.AddRange(Guid.NewGuid(), otherOldDtos, Guid.NewGuid().ToString(), CancellationToken.None); var dtos = createdResult.Item2.Select(e => e.Adapt<ChangeLogValuesDto>());
var oldDtos = GenerateChangeLogCreateRequest(10); // act
await client.AddRange(idDiscriminator, oldDtos, Guid.NewGuid().ToString(), CancellationToken.None); var result = await client.ClearAndAddRange(idDiscriminator, dtos, new CancellationToken());
var newDtos = GenerateChangeLogCreateRequest(insertedCount);
//act
var result = await client.ClearAndAddRange(idDiscriminator, newDtos, "Добавление новых элементов и очистка старых", CancellationToken.None);
// assert // assert
Assert.Equal(insertedCount * 2, result); Assert.Equal(insertedCount * 2, result);
var data = await client.GetByDate(idDiscriminator, DateTimeOffset.Now.AddSeconds(1), new PaginationRequest { Take = 10 * insertedCount }, CancellationToken.None); }
Assert.Equal(insertedCount, data.Count);
[Fact]
public async Task Add_returns_success()
{
// arrange
var count = 1;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count);
var dto = dtos.FirstOrDefault()!;
// act
var result = await client.Add(idDiscriminator, dto, new CancellationToken());
// assert
Assert.Equal(count, result);
} }
[Fact] [Fact]
@ -66,11 +81,10 @@ public class ChangeLogControllerTest : BaseIntegrationTest
// arrange // arrange
var count = 3; var count = 3;
var idDiscriminator = Guid.NewGuid(); var idDiscriminator = Guid.NewGuid();
var dtos = GenerateChangeLogCreateRequest(count); var dtos = Generate(count);
var comment = "Создаю 3 элемента";
// act // act
var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None); var result = await client.AddRange(idDiscriminator, dtos, new CancellationToken());
// assert // assert
Assert.Equal(count, result); Assert.Equal(count, result);
@ -79,42 +93,44 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact] [Fact]
public async Task Update_returns_success() public async Task Update_returns_success()
{ {
//arrange // arrange
var begin = DateTimeOffset.UtcNow; dbContext.CleanupDbSet<ChangeLog>();
var idDiscriminator = Guid.NewGuid();
var dtos = GenerateChangeLogCreateRequest(1);
var original = dtos.First();
var comment = "Создаю 1 элемент";
var result = await client.AddRange(idDiscriminator, [original], comment, CancellationToken.None);
var query = dbContext.Set<ChangeLog>() var idDiscriminator = Guid.NewGuid();
.Include(x => x.CreatedCommit) var dtos = Generate(1);
.Where(x => x.CreatedCommit.DiscriminatorId == idDiscriminator); var dto = dtos.FirstOrDefault()!;
var entity = await query.FirstAsync(); var result = await client.Add(idDiscriminator, dto, new CancellationToken());
var modified = entity.Adapt<ChangeLogBaseDto>();
var key = modified.Value.First().Key; var entity = dbContext.ChangeLog
modified.Value[key] = random.NextDouble(); .Where(x => x.IdDiscriminator == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<ChangeLogValuesDto>();
// act // act
comment = "Обновляю 1 элемент"; result = await client.Update(dto, new CancellationToken());
result = await client.UpdateRange(idDiscriminator, [modified], comment, CancellationToken.None);
// assert // assert
Assert.Equal(2, result); Assert.Equal(2, result);
var now = DateTimeOffset.UtcNow.AddSeconds(1); var dateBegin = DateTimeOffset.UtcNow.AddDays(-1);
var dateEnd = DateTimeOffset.UtcNow.AddDays(1);
var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, begin, now, new CancellationToken()); var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, new CancellationToken());
Assert.NotNull(changeLogResult); Assert.NotNull(changeLogResult);
var obsoleteDto = changeLogResult var obsoleteDto = changeLogResult
.Where(e => e.Obsolete.HasValue) .Where(e => e.Obsolete.HasValue)
.First(); .FirstOrDefault();
var activeDto = changeLogResult var activeDto = changeLogResult
.Where(e => !e.Obsolete.HasValue) .Where(e => !e.Obsolete.HasValue)
.OrderByDescending(e => e.Creation) .FirstOrDefault();
.First();
if (obsoleteDto == null || activeDto == null)
{
Assert.Fail();
return;
}
Assert.Equal(activeDto.Id, obsoleteDto.IdNext); Assert.Equal(activeDto.Id, obsoleteDto.IdNext);
@ -123,321 +139,193 @@ public class ChangeLogControllerTest : BaseIntegrationTest
[Fact] [Fact]
public async Task UpdateRange_returns_success() public async Task UpdateRange_returns_success()
{ {
// arrange
var count = 2; var count = 2;
var idDiscriminator = Guid.NewGuid(); var dtos = Generate(count);
var dtos = GenerateChangeLogCreateRequest(count); var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
var comment = "Создаю 3 элемента"; dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new ChangeLogValuesDto()
{
Id = c.Id,
Value = c.Value
}).ToArray();
// act // act
var result = await client.AddRange(idDiscriminator, dtos, comment, CancellationToken.None); var result = await client.UpdateRange(dtos, new CancellationToken());
var paginatedResult = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddDays(1), paginationRequest, CancellationToken.None);
// act
comment = "Обновляю 3 элемента";
result = await client.UpdateRange(idDiscriminator, paginatedResult.Items, comment, CancellationToken.None);
// assert // assert
Assert.Equal(count * 2, result); Assert.Equal(count * 2, result);
} }
[Fact]
public async Task Delete_returns_success()
{
// arrange
var dtos = Generate(1);
var dto = dtos.FirstOrDefault()!;
var entity = dto.Adapt<ChangeLog>();
dbContext.ChangeLog.Add(entity);
dbContext.SaveChanges();
// act
var result = await client.Delete(entity.Id, new CancellationToken());
// assert
Assert.Equal(1, result);
}
[Fact] [Fact]
public async Task DeleteRange_returns_success() public async Task DeleteRange_returns_success()
{ {
// arrange // arrange
var insertedCount = 10; var count = 10;
var idDiscriminator = Guid.NewGuid(); var dtos = Generate(count);
var otherOldDtos = GenerateChangeLogCreateRequest(10); var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
await client.AddRange(Guid.NewGuid(), otherOldDtos, Guid.NewGuid().ToString(), CancellationToken.None); dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
var oldDtos = GenerateChangeLogCreateRequest(10);
await client.AddRange(idDiscriminator, oldDtos, Guid.NewGuid().ToString(), CancellationToken.None);
var existing = await client.GetByDate(idDiscriminator, DateTimeOffset.UtcNow.AddSeconds(1), new PaginationRequest { Take= 100*insertedCount}, CancellationToken.None);
var ids = existing.Items.Select(e => e.Id);
// act // act
var result = await client.DeleteRange(idDiscriminator, ids, "Удаление нескольких записей", CancellationToken.None); var ids = entities.Select(e => e.Id);
var result = await client.DeleteRange(ids, new CancellationToken());
// assert // assert
Assert.Equal(insertedCount, result); Assert.Equal(count, result);
var items = await dbContext.Set<ChangeLog>()
.Where(e=> ids.Contains(e.Id))
.ToArrayAsync(CancellationToken.None);
Assert.Equal(insertedCount, items.Count());
Assert.All(items, i => Assert.NotNull(i.IdObsoletedCommit));
}
static ChangeLogCommit GenerateCommit(Guid discriminatorId, DateTimeOffset creation)
=> GenerateCommit(discriminatorId, Guid.NewGuid(), creation);
static ChangeLogCommit GenerateCommit(Guid discriminatorId, Guid userId, DateTimeOffset creation)
=> new ChangeLogCommit
{
Id = Guid.NewGuid(),
DiscriminatorId = discriminatorId,
IdAuthor = userId,
Creation = creation,
Comment = Guid.NewGuid().ToString(),
};
static IEnumerable<ChangeLog> GenerateChangeLog(int count, ChangeLogCommit createCommit)
=> GenerateChangeLog(count, createCommit, null, null);
static IEnumerable<ChangeLog> GenerateChangeLog(int count, ChangeLogCommit createCommit, ChangeLogCommit? obsoleteCommit, Guid? idNext)
{
for (var i = 0; i< count; i++)
{
yield return new ChangeLog
{
Id = Guid.NewGuid(),
CreatedCommit = createCommit,
IdCreatedCommit = createCommit.Id,
ObsoletedCommit = obsoleteCommit,
IdObsoletedCommit = obsoleteCommit?.Id,
IdNext = idNext,
Value = new Dictionary<string, object> { { "Guid.NewGuid()", random.NextDouble() } }
};
}
}
static ChangeLogCommit GenerateCommitWithNewData(Guid discriminatorId, DateTimeOffset creation, int count)
{
var commit = GenerateCommit(discriminatorId, creation);
commit.ChangeLogCreatedItems = GenerateChangeLog(count, commit).ToList();
return commit;
}
async Task<ChangeLogCommit> SeedCommitWithNewData(Guid discriminatorId, DateTimeOffset creation, int count)
{
var data = GenerateCommitWithNewData(discriminatorId, creation, count);
dbContext.Set<ChangeLogCommit>().Add(data);
await dbContext.SaveChangesAsync();
return data;
} }
[Fact] [Fact]
public async Task GetDatesRange_returns_success() public async Task GetDatesRange_returns_success()
{ {
//arrange // arrange
var dateMin = DateTimeOffset.UtcNow.AddDays(-1); var changeLogItems = CreateChangeLogItems(3, (-15, 15));
var dateMax = DateTimeOffset.UtcNow; var idDiscriminator = changeLogItems.Item1;
var dateMid = dateMin + 0.5 * (dateMax - dateMin); var entities = changeLogItems.Item2.OrderBy(e => e.Creation);
var idDiscriminator = Guid.NewGuid();
var idOtherDiscriminator = Guid.NewGuid();
var count = 15;
await SeedCommitWithNewData(idDiscriminator, dateMin, count);
await SeedCommitWithNewData(idDiscriminator, dateMid, count);
await SeedCommitWithNewData(idDiscriminator, dateMax, count);
await SeedCommitWithNewData(idOtherDiscriminator, dateMin.AddDays(-1), count);
// act // act
var result = await client.GetDatesRange(idDiscriminator, CancellationToken.None); var result = await client.GetDatesRange(idDiscriminator, new CancellationToken());
// assert // assert
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(dateMin, result.From, TimeSpan.FromSeconds(1));
Assert.Equal(dateMax, result.To, TimeSpan.FromSeconds(1)); var minDate = entities.First().Creation;
var maxDate = entities.Last().Creation;
var expectedMinDate = minDate.ToUniversalTime().ToString();
var actualMinDate = result.From.ToUniversalTime().ToString();
Assert.Equal(expectedMinDate, actualMinDate);
var expectedMaxDate = maxDate.ToUniversalTime().ToString();
var actualMaxDate = result.To.ToUniversalTime().ToString();
Assert.Equal(expectedMaxDate, actualMaxDate);
} }
[Fact] [Fact]
public async Task GetByDate_returns_success() public async Task GetByDate_returns_success()
{ {
// arrange // arrange
var dbSetCommit = dbContext.Set<ChangeLogCommit>(); dbContext.CleanupDbSet<ChangeLog>();
var dbSet = dbContext.Set<ChangeLog>();
var idDiscriminator = Guid.NewGuid(); //создаем записи
var date = DateTimeOffset.UtcNow; var count = 5;
var expected = 10; var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var skip = 2; var idDiscriminator = changeLogItems.Item1;
var take = skip + 2; var entities = changeLogItems.Item2;
var count = expected + take;
var commit = GenerateCommit(idDiscriminator, date);
dbSetCommit.Add(commit);
var commitData = GenerateChangeLog(count, commit);
dbSet.AddRange(commitData);
await dbContext.SaveChangesAsync(CancellationToken.None); //удаляем все созданные записи за исключением первой и второй
var dataWithIds = await dbSet //даты 2-х оставшихся записей должны вернуться в методе GetByDate
.Where(e=>e.CreatedCommit.DiscriminatorId == idDiscriminator) var ids = entities.Select(e => e.Id);
.AsNoTracking() var idsToDelete = ids.Skip(2);
.ToArrayAsync(CancellationToken.None);
var itemsToDelete = dataWithIds.Skip(skip).Take(take); var deletedCount = await client.DeleteRange(idsToDelete, new CancellationToken());
var idsToDelete = itemsToDelete.Select(x => x.Id).ToArray();
// act var paginationRequest = new PaginationRequest()
var deletedCount = await client.DeleteRange(idDiscriminator, idsToDelete, "Удаление нескольких записей", CancellationToken.None); {
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
// assert var moment = DateTimeOffset.UtcNow.AddDays(16);
var moment = DateTimeOffset.UtcNow.AddSeconds(1); var result = await client.GetByDate(idDiscriminator, moment, paginationRequest, new CancellationToken());
var result = await client.GetByDate(idDiscriminator, moment, new() { Take = count *100}, new CancellationToken());
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(expected, result.Items.Count());
var restEntities = entities.Where(e => !idsToDelete.Contains(e.Id));
Assert.Equal(restEntities.Count(), result.Count);
var actualIds = restEntities.Select(e => e.Id);
var expectedIds = result.Items.Select(e => e.Id);
Assert.Equivalent(expectedIds, actualIds);
} }
[Fact] [Theory]
public async Task GetChangeLogForInterval_returns_success() [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 // arrange
var dateMin = DateTimeOffset.UtcNow.AddDays(- 11); dbContext.CleanupDbSet<ChangeLog>();
var dateMax = DateTimeOffset.UtcNow;
var dateMid = dateMin + 0.5 * (dateMax - dateMin);
var idDiscriminator = Guid.NewGuid();
var idOtherDiscriminator = Guid.NewGuid();
var count = 17;
var commitMin = await SeedCommitWithNewData(idDiscriminator, dateMin, count); //создаем записи
var commitMid = await SeedCommitWithNewData(idDiscriminator, dateMid, count); var count = insertedCount;
await SeedCommitWithNewData(idDiscriminator, dateMax, count); var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
await SeedCommitWithNewData(idOtherDiscriminator, dateMin.AddDays(-1), count); var changeLogItems = CreateChangeLogItems(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
var dateBegin = commitMin.Creation; var dtos = entities.Select(e => e.Adapt<ChangeLogValuesDto>()).ToArray();
var dateEnd = commitMid.Creation.AddSeconds(1); await client.UpdateRange(dtos, new CancellationToken());
var expectedCount = commitMin.ChangeLogCreatedItems.Count()
+ commitMid.ChangeLogCreatedItems.Count();
//act
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, CancellationToken.None);
//assert
Assert.Equal(expectedCount, result.Count());
}
[Fact]
public async Task GetStatistics_returns_success()
{
// arrange
var discriminatorId = Guid.NewGuid();
// создаем записи
var date0 = DateTimeOffset.UtcNow.AddDays(-17);
var day0 = new DateOnly(date0.Year, date0.Month, date0.Day);
var inserted0 = 17;
var commit0 = await SeedCommitWithNewData(discriminatorId, date0, inserted0);
// создаем еще записи
var date1 = DateTimeOffset.UtcNow.AddDays(-7);
var day1 = new DateOnly(date1.Year, date1.Month, date1.Day);
var inserted1 = inserted0 + 5;
var commit1 = await SeedCommitWithNewData(discriminatorId, date1, inserted1);
// создаем еще записи с новым дискриминатором
await SeedCommitWithNewData(Guid.NewGuid(), date1, 17);
// обновим
var date2 = DateTimeOffset.UtcNow;
var day2 = new DateOnly(date2.Year, date2.Month, date2.Day);
var updated = commit0.ChangeLogCreatedItems.Select(i => new ChangeLogBaseDto { Id = i.Id, Value = i.Value });
await client.UpdateRange(commit0.DiscriminatorId, updated, Guid.NewGuid().ToString(), CancellationToken.None);
// удалим из коммита
var idToDelete = commit1.ChangeLogCreatedItems.First().Id;
await client.DeleteRange(commit1.DiscriminatorId, [idToDelete], Guid.NewGuid().ToString(), CancellationToken.None);
//act //act
var request = new ChangeLogQuery() var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter);
{ var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter);
DiscriminatorId = commit0.DiscriminatorId, var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, new CancellationToken());
UserId = null
};
var statistics = await client.GetStatistics(request, CancellationToken.None);
//assert //assert
Assert.Equal(3, statistics.Count()); Assert.NotNull(result);
Assert.Equal(changeLogCount, result.Count());
var stat0 = statistics.First(s => s.Date == day0);
Assert.Equal(inserted0, stat0.CreatedChangeLogCount);
Assert.Equal(0, stat0.ObsoletedCount);
Assert.Equal(1, stat0.CommitCount);
var stat1 = statistics.First(s => s.Date == day1);
Assert.Equal(inserted1, stat1.CreatedChangeLogCount);
Assert.Equal(0, stat1.ObsoletedCount);
Assert.Equal(1, stat1.CommitCount);
var stat2 = statistics.First(s => s.Date == day2);
Assert.Equal(inserted0, stat2.CreatedChangeLogCount);
Assert.Equal(inserted0 + 1, stat2.ObsoletedCount);
Assert.Equal(2, stat2.CommitCount);
} }
[Fact]
public async Task GetHistory_returns_success()
{
// arrange
var discriminatorId = Guid.NewGuid();
// создаем записи
var date0 = DateTimeOffset.UtcNow.AddDays(-17);
var inserted0 = 17;
var commit0 = await SeedCommitWithNewData(discriminatorId, date0, inserted0);
// обновим private static IEnumerable<ChangeLogValuesDto> Generate(int count)
var date2 = DateTimeOffset.UtcNow;
var updated = commit0.ChangeLogCreatedItems.Skip(1).Select(i => new ChangeLogBaseDto { Id = i.Id, Value = i.Value });
await client.UpdateRange(commit0.DiscriminatorId, updated, Guid.NewGuid().ToString(), CancellationToken.None);
// удалим из коммита
var idToDelete = commit0.ChangeLogCreatedItems.First().Id;
await client.DeleteRange(commit0.DiscriminatorId, [idToDelete], Guid.NewGuid().ToString(), CancellationToken.None);
var request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null
};
var history = await client.GetHistory(request, CancellationToken.None);
Assert.Equal(3, history.Count());
var items = history.OrderBy(e => e.Creation);
var firstItem = items.First();
Assert.Equal(date0.DateTime.ToString(), firstItem.Creation.DateTime.ToString());
Assert.Equal(inserted0, firstItem.ChangeLogCreatedItems.Count());
Assert.Empty(firstItem.ChangeLogObsoletedItems);
var middleItem = items.Skip(1).Take(1).First();
Assert.Equal(date2.DateTime.ToString(), middleItem.Creation.DateTime.ToString());
Assert.Equal(updated.Count(), middleItem.ChangeLogObsoletedItems.Count());
Assert.Equal(updated.Count(), middleItem.ChangeLogCreatedItems.Count());
var lastItem = items.Last();
Assert.Equal(date2.DateTime.ToString(), lastItem.Creation.DateTime.ToString());
Assert.Single(lastItem.ChangeLogObsoletedItems);
//тест случая, когда данных за указанный период нет
request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null,
GeDate = DateTimeOffset.UtcNow.AddMinutes(1),
LeDate = DateTimeOffset.UtcNow.AddDays(10),
};
history = await client.GetHistory(request, CancellationToken.None);
Assert.Empty(history);
//тест случая, когда данные за указанный период есть
request = new ChangeLogQuery()
{
DiscriminatorId = commit0.DiscriminatorId,
UserId = null,
GeDate = DateTimeOffset.UtcNow.AddDays(-17).AddMinutes(-10),
LeDate = DateTimeOffset.UtcNow.AddDays(-1),
};
history = await client.GetHistory(request, CancellationToken.None);
Assert.Single(history);
var createdChangeLogs = history.First().ChangeLogCreatedItems;
Assert.Equal(17, createdChangeLogs.Count());
}
private static IEnumerable<IDictionary<string, object>> GenerateChangeLogCreateRequest(int count)
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
yield return new Dictionary<string, object>() yield return new ChangeLogValuesDto()
{
Value = new Dictionary<string, object>()
{ {
{ "Key", random.NextDouble() } { "Key", 1 }
}; },
Id = 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);
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,10 +1,10 @@
using DD.Persistence.Client; using DD.Persistence.Client;
using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using Xunit; using Xunit;
@ -13,14 +13,17 @@ namespace DD.Persistence.IntegrationTests.Controllers
public class SetpointControllerTest : BaseIntegrationTest public class SetpointControllerTest : BaseIntegrationTest
{ {
private readonly ISetpointClient setpointClient; private readonly ISetpointClient setpointClient;
private readonly SetpointConfigStorage configStorage;
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
{ {
var refitClientFactory = scope.ServiceProvider var refitClientFactory = scope.ServiceProvider
.GetRequiredService<IRefitClientFactory<IRefitSetpointClient>>(); .GetRequiredService<IRefitClientFactory<IRefitSetpointClient>>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<SetpointClient>>();
setpointClient = scope.ServiceProvider setpointClient = scope.ServiceProvider
.GetRequiredService<ISetpointClient>(); .GetRequiredService<ISetpointClient>();
configStorage = (SetpointConfigStorage)scope.ServiceProvider.GetRequiredService<ISetpointConfigStorage>();
} }
@ -29,16 +32,12 @@ namespace DD.Persistence.IntegrationTests.Controllers
{ {
var id = Guid.Parse("e0fcad22-1761-476e-a729-a3c59d51ba41"); var id = Guid.Parse("e0fcad22-1761-476e-a729-a3c59d51ba41");
var config = new MappingConfig(); configStorage.AddOrReplace(id, typeof(float));
config[id] = typeof(float);
var setpointMapper = new SetpointMappingClient(setpointClient, config);
await setpointClient.Add(id, 48.3f, CancellationToken.None); await setpointClient.Add(id, 48.3f, CancellationToken.None);
//act //act
var response = await setpointMapper.GetCurrent([id], CancellationToken.None); var response = await setpointClient.GetCurrent([id], CancellationToken.None);
//assert //assert
Assert.NotNull(response); Assert.NotNull(response);

View File

@ -3,6 +3,7 @@ using DD.Persistence.Client.Clients;
using DD.Persistence.Client.Clients.Interfaces; using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Interfaces.Refit; using DD.Persistence.Client.Clients.Interfaces.Refit;
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Extensions;
using DD.Persistence.Models; using DD.Persistence.Models;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -38,7 +39,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
} }
[Fact] [Fact]
public async Task Get_returns_BadRequest() public async Task Get_returns_success()
{ {
//arrange //arrange
Cleanup(); Cleanup();
@ -49,18 +50,11 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var secondDiscriminatorId = Guid.NewGuid(); var secondDiscriminatorId = Guid.NewGuid();
discriminatorIds.Append(secondDiscriminatorId); discriminatorIds.Append(secondDiscriminatorId);
try //act
{ var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, 0, 1, CancellationToken.None);
//act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], null, null, null, 0, 1, CancellationToken.None);
}
catch (Exception ex)
{
var expectedMessage = $"На сервере произошла ошибка, в результате которой он не может успешно обработать запрос";
//assert //assert
Assert.Equal(expectedMessage, ex.Message); Assert.Null(response);
}
} }
[Fact] [Fact]
@ -77,25 +71,22 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1); var timestampBegin = DateTimeOffset.UtcNow.AddDays(-1);
var columnNames = new List<string>() { "A", "C" }; var columnNames = new List<string>() { "A", "C" };
var skip = 0; var skip = 2;
var take = 6; // Ровно столько значений будет удовлетворять фильтру (\"A\">3) (для одного дискриминатора) var take = 16;
var customFilter = "(\"A\">3)";
var dtos = (await AddRange(firstDiscriminatorId)).ToList(); var dtos = (await AddRange(firstDiscriminatorId)).ToList();
dtos.AddRange(await AddRange(secondDiscriminatorId)); dtos.AddRange(await AddRange(secondDiscriminatorId));
//act //act
var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId], var response = await timestampedValuesClient.Get([firstDiscriminatorId, secondDiscriminatorId],
timestampBegin, customFilter, columnNames, skip, take, CancellationToken.None); timestampBegin, columnNames, skip, take, CancellationToken.None);
//assert //assert
Assert.NotNull(response); Assert.NotNull(response);
Assert.NotEmpty(response); Assert.NotEmpty(response);
var expectedCount = take * 2;
var actualCount = response.Count(); var actualCount = response.Count();
Assert.Equal(expectedCount, actualCount); Assert.Equal(take, actualCount);
var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList(); var actualColumnNames = response.SelectMany(e => e.Values.Keys).Distinct().ToList();
Assert.Equal(columnNames, actualColumnNames); Assert.Equal(columnNames, actualColumnNames);
@ -388,7 +379,7 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
var response = await timestampedValuesClient.AddRange(discriminatorId, generatedDtos, CancellationToken.None); var response = await timestampedValuesClient.AddRange(discriminatorId, generatedDtos, CancellationToken.None);
// assert // assert
//Assert.Equal(generatedDtos.Count(), response); Assert.Equal(generatedDtos.Count(), response);
return generatedDtos; return generatedDtos;
} }
@ -416,12 +407,8 @@ public class TimestampedValuesControllerTest : BaseIntegrationTest
private void Cleanup() private void Cleanup()
{ {
foreach (var item in discriminatorIds)
{
memoryCache.Remove(item);
}
discriminatorIds = []; discriminatorIds = [];
dbContext.CleanupDbSet<TimestampedValues>(); dbContext.CleanupDbSet<TimestampedValues>();
dbContext.CleanupDbSet<SchemeProperty>(); dbContext.CleanupDbSet<DataScheme>();
} }
} }

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.Configuration; using DD.Persistence.Client.Helpers;
using Microsoft.Extensions.Configuration;
namespace DD.Persistence.IntegrationTests namespace DD.Persistence.IntegrationTests
{ {

View File

@ -5,9 +5,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using DD.Persistence.API;
using DD.Persistence.Client; using DD.Persistence.Client;
using DD.Persistence.Database.Model; using DD.Persistence.Database.Model;
using DD.Persistence.App; using DD.Persistence.App;
using DD.Persistence.Client.Helpers;
using DD.Persistence.Factories;
using System.Net;
using DD.Persistence.Database.Postgres.Extensions; using DD.Persistence.Database.Postgres.Extensions;
namespace DD.Persistence.IntegrationTests; namespace DD.Persistence.IntegrationTests;
@ -20,9 +24,10 @@ public class WebAppFactoryFixture : WebApplicationFactory<Program>
builder.ConfigureAppConfiguration((hostingContext, config) => builder.ConfigureAppConfiguration((hostingContext, config) =>
{ {
config.AddJsonFile("appsettings.Tests.json"); config.AddJsonFile("appsettings.Tests.json");
var configurationRoot = config.Build();
var dbConnection = configurationRoot.GetSection("DbConnection").Get<DbConnection>()!; var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString(); connectionString = dbConnection.GetConnectionString();
//connectionString = "Host=postgres;Port=5442;Username=postgres;Password=postgres;Database=persistence";
}); });
builder.ConfigureServices(services => builder.ConfigureServices(services =>

View File

@ -1,44 +0,0 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель для создания коммита
/// </summary>
public class ChangeLogCommitCreateRequest
{
/// <summary>
/// DiscriminatorId
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Пользователь, совершающий коммит
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Комментарий
/// </summary>
public string Comment { get; set; } = null!;
/// <summary>
/// ctor
/// </summary>
public ChangeLogCommitCreateRequest()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="discriminatorId"></param>
/// <param name="idAuthor"></param>
/// <param name="comment"></param>
public ChangeLogCommitCreateRequest(Guid discriminatorId, Guid idAuthor, string comment)
{
DiscriminatorId = discriminatorId;
IdAuthor = idAuthor;
Comment = comment;
}
}

View File

@ -1,27 +0,0 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель коммита с изменениями
/// </summary>
public class ChangeLogCommitDto : ChangeLogCommitCreateRequest
{
/// <summary>
/// Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Список созданных записей коммита
/// </summary>
public IEnumerable<ChangeLogDto> ChangeLogCreatedItems { get; set; } = null!;
/// <summary>
/// Список устаревших записей коммита
/// </summary>
public IEnumerable<ChangeLogDto> ChangeLogObsoletedItems { get; set; } = null!;
}

View File

@ -1,28 +0,0 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Запрос, используемый для получения данных по журналу операций
/// </summary>
public class ChangeLogQuery
{
/// <summary>
/// Дискриминатор задачи
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary>
/// Пользователь
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Больше или равно указанной даты
/// </summary>
public DateTimeOffset? GeDate { get; set; }
/// <summary>
/// Меньше или равно указанной даты
/// </summary>
public DateTimeOffset? LeDate { get; set; }
}

View File

@ -1,27 +0,0 @@
namespace DD.Persistence.Models.ChangeLog;
/// <summary>
/// Модель, необходимая для отображения статистики по журналу изменений
/// </summary>
public class ChangeLogStatisticsDto
{
/// <summary>
/// Дата изменений
/// </summary>
public DateOnly Date { get; set; }
/// <summary>
/// Количество созданных элементов журнала изменений
/// </summary>
public int CreatedChangeLogCount { get; set; }
/// <summary>
/// Количество устаревших элементов журнала изменений
/// </summary>
public int ObsoletedCount { get; set; }
/// <summary>
/// Количество коммитов
/// </summary>
public int CommitCount { get; set; }
}

View File

@ -1,14 +1,19 @@
namespace DD.Persistence.Models.ChangeLog; namespace DD.Persistence.Models;
/// <summary> /// <summary>
/// Часть записи описывающая изменение /// Часть записи описывающая изменение
/// </summary> /// </summary>
public class ChangeLogDto: ChangeLogBaseDto public class ChangeLogDto
{ {
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary> /// <summary>
/// Создатель записи /// Создатель записи
/// </summary> /// </summary>
public Guid? IdAuthor { get; set; } public Guid IdAuthor { get; set; }
/// <summary> /// <summary>
/// Пользователь, изменивший запись /// Пользователь, изменивший запись
@ -18,7 +23,7 @@ public class ChangeLogDto: ChangeLogBaseDto
/// <summary> /// <summary>
/// Дата создания /// Дата создания
/// </summary> /// </summary>
public DateTimeOffset? Creation { get; set; } public DateTimeOffset Creation { get; set; }
/// <summary> /// <summary>
/// Дата устаревания /// Дата устаревания
@ -29,4 +34,9 @@ public class ChangeLogDto: ChangeLogBaseDto
/// Ключ заменившей записи /// Ключ заменившей записи
/// </summary> /// </summary>
public Guid? IdNext { get; set; } public Guid? IdNext { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public ChangeLogValuesDto Value { get; set; } = default!;
} }

View File

@ -1,9 +1,9 @@
namespace DD.Persistence.Models.ChangeLog; namespace DD.Persistence.Models;
/// <summary> /// <summary>
/// Dto для хранения записей /// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// </summary> /// </summary>
public class ChangeLogBaseDto public class ChangeLogValuesDto
{ {
/// <summary> /// <summary>
/// Ключ записи /// Ключ записи

View File

@ -1,4 +0,0 @@
namespace DD.Persistence.Models.Configurations;
public class MappingConfig : Dictionary<Guid, Type>
{
}

View File

@ -1,11 +1,9 @@
using System.Collections; namespace DD.Persistence.Models;
namespace DD.Persistence.Models;
/// <summary> /// <summary>
/// Схема для набора данных /// Схема для набора данных
/// </summary> /// </summary>
public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumerable<SchemePropertyDto>> public class DataSchemeDto
{ {
/// <summary> /// <summary>
/// Дискриминатор /// Дискриминатор
@ -13,30 +11,7 @@ public class DataSchemeDto : IEnumerable<SchemePropertyDto>, IEquatable<IEnumera
public Guid DiscriminatorId { get; set; } public Guid DiscriminatorId { get; set; }
/// <summary> /// <summary>
/// Поля /// Наименования полей
/// </summary> /// </summary>
private IEnumerable<SchemePropertyDto> Properties { get; } = []; public string[] PropNames { get; set; } = [];
/// <inheritdoc/>
public DataSchemeDto(Guid discriminatorId, IEnumerable<SchemePropertyDto> Properties)
{
DiscriminatorId = discriminatorId;
this.Properties = Properties;
}
/// <inheritdoc/>
public IEnumerator<SchemePropertyDto> GetEnumerator()
=> Properties.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public bool Equals(IEnumerable<SchemePropertyDto>? otherProperties)
{
if (otherProperties is null)
return false;
return Properties.SequenceEqual(otherProperties);
}
} }

View File

@ -1,30 +0,0 @@
using System.Text.Json;
namespace DD.Persistence.Models;
/// <summary>
/// Индексируемого поле из схемы для набора данных
/// </summary>
public class SchemePropertyDto : IEquatable<SchemePropertyDto>
{
/// <summary>
/// Индекс поля
/// </summary>
public required int Index { get; set; }
/// <summary>
/// Наименование индексируемого поля
/// </summary>
public required string PropertyName { get; set; }
/// <summary>
/// Тип индексируемого поля
/// </summary>
public required JsonValueKind PropertyKind { get; set; }
/// <inheritdoc/>
public bool Equals(SchemePropertyDto? other)
{
return Index == other?.Index && PropertyName == other?.PropertyName && PropertyKind == other?.PropertyKind;
}
}

View File

@ -7,11 +7,6 @@ namespace DD.Persistence.Models;
/// </summary> /// </summary>
public class TimestampedValuesDto : ITimestampAbstractDto public class TimestampedValuesDto : ITimestampAbstractDto
{ {
/// <summary>
/// Дискриминатор
/// </summary>
public Guid DiscriminatorId { get; set; }
/// <summary> /// <summary>
/// Временная отметка /// Временная отметка
/// </summary> /// </summary>

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DD.Persistence.Models;
/// <summary>
/// Класс, описывающий пользователя
/// </summary>
public class UserDto
{
/// <summary>
/// Идентификатор пользователя
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Имя пользователя для отображения
/// </summary>
public required string DisplayName { get; set; }
}

View File

@ -11,8 +11,8 @@
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Shouldly" Version="4.2.1" /> <PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="Testcontainers" Version="4.2.0" /> <PackageReference Include="Testcontainers" Version="4.1.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.2.0" /> <PackageReference Include="Testcontainers.PostgreSql" Version="4.1.0" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" /> <PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
@ -20,6 +20,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" /> <ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,7 +1,12 @@
using DD.Persistence.Database.Model; using DD.Persistence.Database.Model;
using DD.Persistence.Database.Postgres.Repositories; using DD.Persistence.Repository.Repositories;
using Shouldly; using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
namespace DD.Persistence.Repository.Test; namespace DD.Persistence.Repository.Test;
public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture> public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
@ -24,6 +29,7 @@ public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
var value = GetJsonFromObject(22); var value = GetJsonFromObject(22);
await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None); await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None);
var t = fixture.dbContainer.GetConnectionString();
//act //act
var result = await sut.GetCurrent([id], CancellationToken.None); var result = await sut.GetCurrent([id], CancellationToken.None);

View File

@ -1,6 +1,6 @@
using System.Collections; using System.Collections;
namespace DD.Persistence.Database.Postgres.Helpers; namespace DD.Persistence.Repository;
/// <summary> /// <summary>
/// Цикличный массив /// Цикличный массив
/// </summary> /// </summary>

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -1,35 +1,44 @@
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Repositories;
using DD.Persistence.Database.Postgres.RepositoriesCached;
using DD.Persistence.Database.Repositories;
using DD.Persistence.Database.RepositoriesCached;
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using DD.Persistence.Repository.Repositories;
using DD.Persistence.Repository.RepositoriesCached;
using Mapster; using Mapster;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Reflection; using System.Reflection;
namespace DD.Persistence.Database; namespace DD.Persistence.Repository;
public static class DependencyInjection public static class DependencyInjection
{ {
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
TypeAdapterConfig<ChangeLog, ChangeLogDto>.NewConfig()
.Map(dest => dest.Value, src => new ChangeLogValuesDto()
{
Value = src.Value,
Id = src.Id
});
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services) public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{ {
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings; var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
typeAdapterConfig.RuleMap.Clear(); typeAdapterConfig.RuleMap.Clear();
typeAdapterConfig.Scan(Assembly.GetExecutingAssembly()); typeAdapterConfig.Scan(Assembly.GetExecutingAssembly());
MapsterSetup.Configure(); MapsterSetup();
services.AddTransient<ISetpointRepository, SetpointRepository>(); services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<IChangeLogCommitRepository, ChangeLogCommitRepository>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>(); services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>(); services.AddTransient<ITimestampedValuesRepository, TimestampedValuesRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>(); services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>(); services.AddTransient<IParameterRepository, ParameterRepository>();
services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>(); services.AddTransient<IDataSourceSystemRepository, DataSourceSystemCachedRepository>();
services.AddTransient<ISchemePropertyRepository, SchemePropertyCachedRepository>(); services.AddTransient<IDataSchemeRepository, DataSchemeCachedRepository>();
return services; return services;
} }

View File

@ -2,7 +2,7 @@ using System.Collections.Concurrent;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
namespace DD.Persistence.Database.Extensions; namespace DD.Persistence.Repository.Extensions;
public static class EFExtensionsSortBy public static class EFExtensionsSortBy
{ {

View File

@ -1,16 +1,30 @@
using DD.Persistence.Database.EntityAbstractions; using Microsoft.EntityFrameworkCore;
using DD.Persistence.Extensions;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests; using DD.Persistence.Models.Requests;
using Microsoft.EntityFrameworkCore; using DD.Persistence.Models.Common;
using DD.Persistence.ModelsAbstractions;
using DD.Persistence.Database.EntityAbstractions;
using DD.Persistence.Extensions;
namespace DD.Persistence.Database.Postgres.Helpers; namespace DD.Persistence.Repository;
/// <summary> /// <summary>
/// класс с набором методов, необходимых для фильтрации записей /// класс с набором методов, необходимых для фильтрации записей
/// </summary> /// </summary>
public static class QueryBuilders public static class QueryBuilders
{ {
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>( public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query, this IQueryable<TEntity> query,
PaginationRequest request, PaginationRequest request,

View File

@ -0,0 +1,255 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
using UuidExtensions;
namespace DD.Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private readonly DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<ChangeLogValuesDto> 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<ChangeLogValuesDto> 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<ChangeLogValuesDto> 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<ChangeLogValuesDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
PaginationRequest paginationRequest,
CancellationToken token)
{
var query = CreateQuery(idDiscriminator);
query = query.Apply(momentUtc);
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 static ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, ChangeLogValuesDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
IdDiscriminator = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value
};
return entity;
}
public async Task<IEnumerable<ChangeLogValuesDto>> 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 ChangeLogValuesDto Convert(ChangeLog entity) => entity.Adapt<ChangeLogValuesDto>();
}

View File

@ -0,0 +1,34 @@
using DD.Persistence.Database.Entity;
using DD.Persistence.Models;
using DD.Persistence.Repositories;
using Mapster;
using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Repository.Repositories;
public class DataSchemeRepository : IDataSchemeRepository
{
protected DbContext db;
public DataSchemeRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<DataScheme> GetQueryReadOnly() => db.Set<DataScheme>();
public virtual async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{
var entity = dataSourceSystemDto.Adapt<DataScheme>();
await db.Set<DataScheme>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
public virtual async Task<DataSchemeDto?> Get(Guid dataSchemeId, CancellationToken token)
{
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == dataSchemeId);
var entity = await query.ToArrayAsync();
var dto = entity.Select(e => e.Adapt<DataSchemeDto>()).FirstOrDefault();
return dto;
}
}

View File

@ -4,7 +4,7 @@ using DD.Persistence.Repositories;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories; namespace DD.Persistence.Repository.Repositories;
public class DataSourceSystemRepository : IDataSourceSystemRepository public class DataSourceSystemRepository : IDataSourceSystemRepository
{ {
protected DbContext db; protected DbContext db;
@ -12,7 +12,7 @@ public class DataSourceSystemRepository : IDataSourceSystemRepository
{ {
this.db = db; this.db = db;
} }
protected IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>(); protected virtual IQueryable<DataSourceSystem> GetQueryReadOnly() => db.Set<DataSourceSystem>();
public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token) public virtual async Task Add(DataSourceSystemDto dataSourceSystemDto, CancellationToken token)
{ {

View File

@ -5,7 +5,7 @@ using DD.Persistence.Models;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
namespace DD.Persistence.Database.Postgres.Repositories; namespace DD.Persistence.Repository.Repositories;
public class ParameterRepository : IParameterRepository public class ParameterRepository : IParameterRepository
{ {
private DbContext db; private DbContext db;
@ -15,7 +15,7 @@ public class ParameterRepository : IParameterRepository
this.db = db; this.db = db;
} }
protected IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>(); protected virtual IQueryable<ParameterData> GetQueryReadOnly() => db.Set<ParameterData>();
public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token) public async Task<DatesRangeDto> GetDatesRangeAsync(Guid idDiscriminator, CancellationToken token)
{ {

View File

@ -6,7 +6,7 @@ using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json; using System.Text.Json;
namespace DD.Persistence.Database.Postgres.Repositories namespace DD.Persistence.Repository.Repositories
{ {
public class SetpointRepository : ISetpointRepository public class SetpointRepository : ISetpointRepository
{ {
@ -16,11 +16,11 @@ namespace DD.Persistence.Database.Postgres.Repositories
this.db = db; this.db = db;
} }
protected IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>(); protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
public async Task<IEnumerable<SetpointValueDto>> GetCurrent( public async Task<IEnumerable<SetpointValueDto>> GetCurrent(
IEnumerable<Guid> setpointKeys, IEnumerable<Guid> setpointKeys,
CancellationToken token) CancellationToken token)
{ {
var query = GetQueryReadOnly(); var query = GetQueryReadOnly();

View File

@ -7,7 +7,7 @@ using DD.Persistence.Repositories;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories namespace DD.Persistence.Repository.Repositories
{ {
public class TechMessagesRepository : ITechMessagesRepository public class TechMessagesRepository : ITechMessagesRepository
{ {
@ -20,7 +20,7 @@ namespace DD.Persistence.Database.Postgres.Repositories
this.sourceSystemRepository = sourceSystemRepository; this.sourceSystemRepository = sourceSystemRepository;
} }
protected 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(PaginationRequest request, CancellationToken token) public async Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token)

View File

@ -1,25 +1,22 @@
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Database.Postgres.Helpers;
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Models.Common; using DD.Persistence.Models.Common;
using DD.Persistence.Repositories; using DD.Persistence.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DD.Persistence.Database.Postgres.Repositories; namespace DD.Persistence.Repository.Repositories;
public class TimestampedValuesRepository : ITimestampedValuesRepository public class TimestampedValuesRepository : ITimestampedValuesRepository
{ {
private readonly DbContext db; private readonly DbContext db;
private readonly ISchemePropertyRepository schemePropertyRepository;
public TimestampedValuesRepository(DbContext db, ISchemePropertyRepository schemePropertyRepository) public TimestampedValuesRepository(DbContext db)
{ {
this.db = db; this.db = db;
this.schemePropertyRepository = schemePropertyRepository;
} }
protected IQueryable<TimestampedValues> GetQueryReadOnly() => db.Set<TimestampedValues>(); protected virtual IQueryable<TimestampedValues> GetQueryReadOnly() => this.db.Set<TimestampedValues>();
public async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token) public async virtual Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
{ {
var timestampedValuesEntities = dtos.Select(dto => new TimestampedValues() var timestampedValuesEntities = dtos.Select(dto => new TimestampedValues()
{ {
@ -28,46 +25,35 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
Values = dto.Values.Values.ToArray() Values = dto.Values.Values.ToArray()
}); });
await db.AddRangeAsync(timestampedValuesEntities, token); await db.Set<TimestampedValues>().AddRangeAsync(timestampedValuesEntities, token);
var result = await db.SaveChangesAsync(token); var result = await db.SaveChangesAsync(token);
return result; return result;
} }
public async Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds, public async virtual Task<IDictionary<Guid, IEnumerable<(DateTimeOffset Timestamp, object[] Values)>>> Get(IEnumerable<Guid> discriminatorIds,
DateTimeOffset? geTimestamp, DateTimeOffset? timestampBegin,
TNode? filterTree,
IEnumerable<string>? columnNames, IEnumerable<string>? columnNames,
int skip, int skip,
int take, int take,
CancellationToken token) CancellationToken token)
{ {
var resultQuery = Array.Empty<TimestampedValues>().AsQueryable(); var query = GetQueryReadOnly()
foreach (var discriminatorId in discriminatorIds) .Where(entity => discriminatorIds.Contains(entity.DiscriminatorId));
// Фильтрация по дате
if (timestampBegin.HasValue)
{ {
var scheme = await schemePropertyRepository.Get(discriminatorId, token); query = ApplyGeTimestamp(query, timestampBegin.Value);
if (scheme == null)
throw new NotSupportedException($"Для переданного дискриминатора {discriminatorId} не была обнаружена схема данных");
var geTimestampUtc = geTimestamp!.Value.ToUniversalTime();
var query = GetQueryReadOnly()
.Where(e => e.DiscriminatorId == discriminatorId)
.Where(entity => entity.Timestamp >= geTimestampUtc);
if (filterTree != null)
query = query.ApplyFilter(scheme, filterTree);
resultQuery = resultQuery.Any() ? resultQuery.Union(query) : query;
} }
var groupedQuery = resultQuery!
.GroupBy(e => e.DiscriminatorId)
.Select(g => KeyValuePair.Create(
g.Key,
g.OrderBy(i => i.Timestamp).Skip(skip).Take(take))
);
var entities = await groupedQuery.ToArrayAsync(token); // Группировка отсортированных значений по DiscriminatorId
var groupQuery = query
.GroupBy(e => e.DiscriminatorId)
.Select(g => KeyValuePair.Create(g.Key, g.OrderBy(i => i.Timestamp).Skip(skip).Take(take)));
var entities = await groupQuery.ToArrayAsync(token);
var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => ( var result = entities.ToDictionary(k => k.Key, v => v.Value.Select(e => (
e.Timestamp, e.Timestamp,
e.Values e.Values
@ -76,7 +62,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result; return result;
} }
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token) public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetFirst(Guid discriminatorId, int takeCount, CancellationToken token)
{ {
var query = GetQueryReadOnly() var query = GetQueryReadOnly()
.OrderBy(e => e.Timestamp) .OrderBy(e => e.Timestamp)
@ -91,7 +77,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result; return result;
} }
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token) public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetLast(Guid discriminatorId, int takeCount, CancellationToken token)
{ {
var query = GetQueryReadOnly() var query = GetQueryReadOnly()
.OrderByDescending(e => e.Timestamp) .OrderByDescending(e => e.Timestamp)
@ -107,7 +93,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
} }
// ToDo: прореживание должно осуществляться до материализации // ToDo: прореживание должно осуществляться до материализации
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData( public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetResampledData(
Guid discriminatorId, Guid discriminatorId,
DateTimeOffset dateBegin, DateTimeOffset dateBegin,
double intervalSec = 600d, double intervalSec = 600d,
@ -128,11 +114,10 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result; return result;
} }
public async Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset gtTimestamp, CancellationToken token) public async virtual Task<IEnumerable<(DateTimeOffset Timestamp, object[] Values)>> GetGtDate(Guid discriminatorId, DateTimeOffset timestampBegin, CancellationToken token)
{ {
var gtTimestampUtc = gtTimestamp.ToUniversalTime();
var query = GetQueryReadOnly() var query = GetQueryReadOnly()
.Where(entity => entity.Timestamp > gtTimestampUtc); .Where(e => e.Timestamp > timestampBegin);
var entities = await query.ToArrayAsync(token); var entities = await query.ToArrayAsync(token);
var result = entities.Select(e => ( var result = entities.Select(e => (
@ -143,7 +128,7 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return result; return result;
} }
public async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token) public async virtual Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
{ {
var query = GetQueryReadOnly() var query = GetQueryReadOnly()
.GroupBy(entity => entity.DiscriminatorId) .GroupBy(entity => entity.DiscriminatorId)
@ -168,12 +153,26 @@ public class TimestampedValuesRepository : ITimestampedValuesRepository
return dto; return dto;
} }
public async Task<int> Count(Guid discriminatorId, CancellationToken token) public virtual Task<int> Count(Guid discriminatorId, CancellationToken token)
{ {
var query = GetQueryReadOnly() var dbSet = db.Set<TimestampedValues>();
.Where(e => e.DiscriminatorId == discriminatorId); var query = dbSet.Where(entity => entity.DiscriminatorId == discriminatorId);
var result = await query.CountAsync(token); return query.CountAsync(token);
}
/// <summary>
/// Применить фильтр по дате
/// </summary>
/// <param name="query"></param>
/// <param name="timestampBegin"></param>
/// <returns></returns>
private IQueryable<TimestampedValues> ApplyGeTimestamp(IQueryable<TimestampedValues> query, DateTimeOffset timestampBegin)
{
var geTimestampUtc = timestampBegin.ToUniversalTime();
var result = query
.Where(entity => entity.Timestamp >= geTimestampUtc);
return result; return result;
} }

View File

@ -1,21 +1,21 @@
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Repository.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using DD.Persistence.Database.Repositories;
namespace DD.Persistence.Database.RepositoriesCached; namespace DD.Persistence.Repository.RepositoriesCached;
public class SchemePropertyCachedRepository : SchemePropertyRepository public class DataSchemeCachedRepository : DataSchemeRepository
{ {
private readonly IMemoryCache memoryCache; private readonly IMemoryCache memoryCache;
public SchemePropertyCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db) public DataSchemeCachedRepository(DbContext db, IMemoryCache memoryCache) : base(db)
{ {
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
} }
public override async Task AddRange(DataSchemeDto dataSourceSystemDto, CancellationToken token) public override async Task Add(DataSchemeDto dataSourceSystemDto, CancellationToken token)
{ {
await base.AddRange(dataSourceSystemDto, token); await base.Add(dataSourceSystemDto, token);
memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto); memoryCache.Set(dataSourceSystemDto.DiscriminatorId, dataSourceSystemDto);
} }

View File

@ -1,10 +1,10 @@
using DD.Persistence.Database.Entity; using DD.Persistence.Database.Entity;
using DD.Persistence.Models; using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Repositories; using DD.Persistence.Repository.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
namespace DD.Persistence.Database.Postgres.RepositoriesCached; namespace DD.Persistence.Repository.RepositoriesCached;
public class DataSourceSystemCachedRepository : DataSourceSystemRepository public class DataSourceSystemCachedRepository : DataSourceSystemRepository
{ {
private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey"; private static readonly string SystemCacheKey = $"{typeof(DataSourceSystem).FullName}CacheKey";

View File

@ -0,0 +1,103 @@
//using DD.Persistence.Models;
//using DD.Persistence.Models.Common;
//using DD.Persistence.Repositories;
//using Microsoft.EntityFrameworkCore;
//namespace DD.Persistence.Repository.Repositories;
//public class TimestampedValuesCachedRepository : TimestampedValuesRepository
//{
// public static TimestampedValuesDto? FirstByDate { get; private set; }
// public static CyclicArray<TimestampedValuesDto> LastData { get; } = new CyclicArray<TimestampedValuesDto>(CacheItemsCount);
// private const int CacheItemsCount = 3600;
// public TimestampedValuesCachedRepository(DbContext db, IDataSourceSystemRepository<ValuesIdentityDto> relatedDataRepository) : base(db, relatedDataRepository)
// {
// //Task.Run(async () =>
// //{
// // var firstDateItem = await base.GetFirst(CancellationToken.None);
// // if (firstDateItem == null)
// // {
// // return;
// // }
// // FirstByDate = firstDateItem;
// // var dtos = await base.GetLast(CacheItemsCount, CancellationToken.None);
// // dtos = dtos.OrderBy(d => d.Timestamp);
// // LastData.AddRange(dtos);
// //}).Wait();
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetGtDate(Guid discriminatorId, DateTimeOffset dateBegin, CancellationToken token)
// {
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// var dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// return dtos;
// }
// var items = LastData
// .Where(i => i.Timestamp >= dateBegin);
// return items;
// }
// public override async Task<int> AddRange(Guid discriminatorId, IEnumerable<TimestampedValuesDto> dtos, CancellationToken token)
// {
// var result = await base.AddRange(discriminatorId, dtos, token);
// if (result > 0)
// {
// dtos = dtos.OrderBy(x => x.Timestamp);
// FirstByDate = dtos.First();
// LastData.AddRange(dtos);
// }
// return result;
// }
// public override async Task<DatesRangeDto?> GetDatesRange(Guid discriminatorId, CancellationToken token)
// {
// if (FirstByDate == null)
// return null;
// return await Task.Run(() =>
// {
// return new DatesRangeDto
// {
// From = FirstByDate.Timestamp,
// To = LastData[^1].Timestamp
// };
// });
// }
// public override async Task<IEnumerable<TimestampedValuesDto>> GetResampledData(
// Guid discriminatorId,
// DateTimeOffset dateBegin,
// double intervalSec = 600d,
// int approxPointsCount = 1024,
// CancellationToken token = default)
// {
// var dtos = LastData.Where(i => i.Timestamp >= dateBegin);
// if (LastData.Count == 0 || LastData[0].Timestamp > dateBegin)
// {
// dtos = await base.GetGtDate(discriminatorId, dateBegin, token);
// }
// var dateEnd = dateBegin.AddSeconds(intervalSec);
// dtos = dtos
// .Where(i => i.Timestamp <= dateEnd);
// var ratio = dtos.Count() / approxPointsCount;
// if (ratio > 1)
// dtos = dtos
// .Where((_, index) => index % ratio == 0);
// return dtos;
// }
//}

View File

@ -1,305 +0,0 @@
using DD.Persistence.Database.Repositories;
using DD.Persistence.Models.ChangeLog;
using DD.Persistence.Models.Common;
using DD.Persistence.Models.Requests;
using DD.Persistence.Repositories;
using DD.Persistence.Services;
using Microsoft.Extensions.Caching.Memory;
using NSubstitute;
using UuidExtensions;
namespace DD.Persistence.Test;
public class ChangeLogTest
{
private readonly IChangeLogCommitRepository changeLogCommitRepository = Substitute.For<IChangeLogCommitRepository>();
private readonly IChangeLogRepository changeLogRepository = Substitute.For<IChangeLogRepository>();
private ChangeLogService service;
public ChangeLogTest()
{
var memoryCache = new MemoryCache(new MemoryCacheOptions());
service = new ChangeLogService(memoryCache, changeLogCommitRepository, changeLogRepository);
}
[Fact]
public async Task AddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Добавление нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogRequests(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.AddRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<IDictionary<string, object>>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var addRangeResult = await service
.AddRange(commitRequest, dtos, CancellationToken.None);
addRangeResult = await service
.AddRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).AddRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task UpdateRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Изменение нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogValuesDto(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.UpdateRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<ChangeLogBaseDto>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
updateRangeResult = await service
.UpdateRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(3).UpdateRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task MarkAsDeleted()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Удаление нескольких значений");
var commit = new ChangeLogCommitDto()
{
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogValuesDto(2);
var dtoIds = dtos.Select(d => d.Id);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.MarkAsDeleted(
Arg.Any<IEnumerable<Guid>>(),
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
markAsDeletedResult = await service
.MarkAsDeleted(dtoIds, commitRequest, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).MarkAsDeleted(dtoIds, commit, CancellationToken.None);
}
[Fact]
public async Task ClearAndAddRange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var commitRequest = new ChangeLogCommitCreateRequest(discriminatorId, Uuid7.Guid(), "Удаление и добавление нескольких значений");
var commit = new ChangeLogCommitDto() {
IdAuthor = commitRequest.IdAuthor,
Comment = commitRequest.Comment
};
commit.Id = Uuid7.Guid();
var dtos = GenerateChangeLogRequests(2);
changeLogCommitRepository.Add(Arg.Any<ChangeLogCommitCreateRequest>(), Arg.Any<CancellationToken>()).Returns(commit);
changeLogRepository
.ClearAndAddRange(
Arg.Any<ChangeLogCommitDto>(),
Arg.Any<IEnumerable<IDictionary<string, object>>>(),
Arg.Any<CancellationToken>())
.Returns(2);
//act
var clearAndAddResult = await service
.ClearAndAddRange(commitRequest, dtos, CancellationToken.None);
clearAndAddResult = await service
.ClearAndAddRange(commitRequest, dtos, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).Add(commitRequest, CancellationToken.None);
await changeLogRepository.Received(2).ClearAndAddRange(Arg.Any<ChangeLogCommitDto>(), dtos, CancellationToken.None);
}
[Fact]
public async Task GetByDate()
{
//arrange
var discriminatorId = Uuid7.Guid();
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 1000
};
var dtos = GenerateChangeLogValuesDto(5);
var items = new PaginationContainer<ChangeLogBaseDto>()
{
Take = paginationRequest.Take,
Skip = paginationRequest.Skip,
Items = dtos,
Count = 10
};
var momentDate = DateTime.UtcNow;
changeLogRepository
.GetByDate(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<PaginationRequest>(),
Arg.Any<CancellationToken>())
.Returns(items);
//act
var actualItems = await service
.GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
//assert
await changeLogRepository.Received(1).GetByDate(discriminatorId, momentDate, paginationRequest, CancellationToken.None);
}
[Fact]
public async Task GetChangeLogForInterval()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dtos = GenerateChangeLogDto(5);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
changeLogCommitRepository
.GetChangeLogForInterval(
Arg.Any<Guid>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).GetChangeLogForInterval(discriminatorId, dateBegin, dateEnd, CancellationToken.None);
}
[Fact]
public async Task GetDatesChange()
{
//arrange
var discriminatorId = Uuid7.Guid();
var dateBegin = DateTimeOffset.UtcNow.AddDays(-5);
var dateEnd = DateTimeOffset.UtcNow;
var dateOnlyBegin = new DateOnly(dateBegin.Year, dateBegin.Month, dateBegin.Day);
var dateOnlyEnd = new DateOnly(dateEnd.Year, dateEnd.Month, dateEnd.Day);
var dtos = new List<DateOnly>() { dateOnlyBegin, dateOnlyEnd };
changeLogCommitRepository
.GetDatesChange(
Arg.Any<Guid>(),
Arg.Any<CancellationToken>())
.Returns(dtos);
//act
var actualItems = await service
.GetDatesChange(discriminatorId, CancellationToken.None);
//assert
await changeLogCommitRepository.Received(1).GetDatesChange(discriminatorId, CancellationToken.None);
}
private IEnumerable<ChangeLogBaseDto> GenerateChangeLogValuesDto(int count)
{
var items = new List<ChangeLogBaseDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogBaseDto()
{
Id = Uuid7.Guid(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
private IEnumerable<IDictionary<string, object>> GenerateChangeLogRequests(int count)
{
var items = new List<IDictionary<string, object>>();
for (int i = 0; i < count; i++)
{
items.Add(new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
});
}
return items;
}
private IEnumerable<ChangeLogDto> GenerateChangeLogDto(int count)
{
var items = new List<ChangeLogDto>();
for (int i = 0; i < count; i++)
{
items.Add(new ChangeLogDto()
{
Id = Uuid7.Guid(),
Value = new Dictionary<string, object>
{
{ "1", 1 },
{ "2", 2 }
}
});
}
return items;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
@ -16,9 +16,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DD.Persistence.API\DD.Persistence.API.csproj" /> <ProjectReference Include="..\DD.Persistence.Database\DD.Persistence.Database.csproj" />
<ProjectReference Include="..\DD.Persistence.Client\DD.Persistence.Client.csproj" /> <ProjectReference Include="..\DD.Persistence\DD.Persistence.csproj" />
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,278 +0,0 @@
using Ardalis.Specification.EntityFrameworkCore;
using DD.Persistence.Database.Entity;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Models;
using DD.Persistence.Database.Postgres.Helpers;
using System.Text.Json;
namespace DD.Persistence.Test;
/// ToDo: переписать под Theory
public class FilterBuilderShould
{
private readonly SpecificationEvaluator SpecificationEvaluator;
public FilterBuilderShould()
{
this.SpecificationEvaluator = new SpecificationEvaluator();
}
[Fact]
public void TestFilterBuilding()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var filterDate = DateTime.Now.AddMinutes(-1);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Greate, "A", filterDate),
new TLeaf(OperationEnum.Less, "B", 2.22)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 2;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterOperations()
{
//arrange
var discriminatorId = Guid.NewGuid();
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TLeaf(OperationEnum.Less, "A", 2),
new TLeaf(OperationEnum.LessOrEqual, "A", 1.99)
),
new TLeaf(OperationEnum.GreateOrEqual, "A", 1.97)
),
new TLeaf(OperationEnum.Greate, "A", 1.96)
),
new TLeaf(OperationEnum.NotEqual, "A", 1.98)
),
new TLeaf(OperationEnum.Equal, "A", 1)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 1.96 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 1.97 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 1.98 } // false
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-5),
Values = new object[] { 1.99 } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-6),
Values = new object[] { 2 } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 3;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
[Fact]
public void TestFilterValues()
{
//arrange
var discriminatorId = Guid.NewGuid();
var filterDate = DateTimeOffset.Now;
var dataSchemeProperties = new SchemePropertyDto[]
{
new SchemePropertyDto()
{
Index = 0,
PropertyName = "A",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 1,
PropertyName = "B",
PropertyKind = JsonValueKind.Number
},
new SchemePropertyDto()
{
Index = 2,
PropertyName = "C",
PropertyKind = JsonValueKind.String
},
new SchemePropertyDto()
{
Index = 3,
PropertyName = "D",
PropertyKind = JsonValueKind.String
}
};
var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties);
var root = new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1),
new TLeaf(OperationEnum.Equal, "B", 1.11)
),
new TLeaf(OperationEnum.Equal, "C", "IsEqualText")
),
new TLeaf(OperationEnum.Equal, "D", filterDate)
);
var queryableData = new[]
{
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-2),
Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-3),
Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-4),
Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate } // true
},
new TimestampedValues {
DiscriminatorId = discriminatorId,
Timestamp = DateTimeOffset.Now.AddMinutes(-1),
Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // false
}
}
.AsQueryable();
//act
queryableData = queryableData.ApplyFilter(dataScheme, root);
//assert
var result = queryableData.ToList();
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotNull(result);
Assert.NotEmpty(result);
var expectedCount = 4;
var actualCount = result.Count();
Assert.Equal(expectedCount, actualCount);
}
}

View File

@ -1,99 +0,0 @@
using DD.Persistence.Client.Clients.Interfaces;
using DD.Persistence.Client.Clients.Mapping;
using DD.Persistence.Client.Clients.Mapping.Clients;
using DD.Persistence.Models;
using DD.Persistence.Models.Configurations;
using Microsoft.Extensions.Logging;
using NSubstitute;
using System.Text.Json;
namespace DD.Persistence.Test;
public record FirstTestDto(Guid DiscriminatorId, DateTimeOffset Timestamp, int Id, string? Value);
public record SecondTestDto(Guid DiscriminatorId, DateTimeOffset Timestamp, int Id, double Capacity);
public class MappingClientsTest
{
private readonly ITimestampedValuesClient timestampedValuesClient = Substitute.For<ITimestampedValuesClient>();
private readonly ILogger<TimestampedSetMapper> logger = Substitute.For<ILogger<TimestampedSetMapper>>();
private readonly TimestampedMappingClient timestampedMappingClient;
private readonly MappingConfig mappingConfigs;
public MappingClientsTest()
{
mappingConfigs = GetConfig();
var storage = new MapperStorage(mappingConfigs, logger);
timestampedMappingClient = new TimestampedMappingClient(timestampedValuesClient, storage);
}
[Fact]
public async Task GetMultiMapped()
{
// Arrange
var discriminatorIds = mappingConfigs.Keys;
var firstDiscriminatorId = discriminatorIds.First();
var secondDiscriminatorId = discriminatorIds.Last();
var getResult = new[]
{
new TimestampedValuesDto()
{
DiscriminatorId = firstDiscriminatorId,
Timestamp = DateTime.UtcNow,
Values = new Dictionary<string, object>
{
{ nameof(FirstTestDto.Id), JsonDocument.Parse(JsonSerializer.Serialize(1)).RootElement },
{ nameof(FirstTestDto.Value), JsonDocument.Parse(JsonSerializer.Serialize("string1")).RootElement}
}
},
new TimestampedValuesDto()
{
DiscriminatorId = secondDiscriminatorId,
Timestamp = DateTime.UtcNow,
Values = new Dictionary<string, object>
{
{ nameof(SecondTestDto.Id), JsonDocument.Parse(JsonSerializer.Serialize(1)).RootElement },
{ nameof(SecondTestDto.Capacity), JsonDocument.Parse(JsonSerializer.Serialize(0.1)).RootElement}
}
}
};
timestampedValuesClient
.Get(discriminatorIds, null, null, null, 0, 1, CancellationToken.None)
.ReturnsForAnyArgs(getResult);
// Act
var result = await timestampedMappingClient.GetMultiMapped(discriminatorIds, null, null, null, 0, 1, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(getResult.Count(), result.Count());
var firstActualDto = (FirstTestDto) result[firstDiscriminatorId].First();
Assert.NotNull(firstActualDto);
var actualId = firstActualDto.Id.ToString();
var expectedId = getResult[0].Values[nameof(FirstTestDto.Id)].ToString();
Assert.Equal(expectedId, actualId);
var secondActualDto = (SecondTestDto) result[secondDiscriminatorId].First();
Assert.NotNull(secondActualDto);
actualId = secondActualDto.Id.ToString();
expectedId = getResult[1].Values[nameof(SecondTestDto.Id)].ToString();
Assert.Equal(expectedId, actualId);
}
private MappingConfig GetConfig()
{
var config = new MappingConfig();
config[Guid.NewGuid()] = typeof(FirstTestDto);
config[Guid.NewGuid()] = typeof(SecondTestDto);
return config;
}
}

Some files were not shown because too many files have changed in this diff Show More