Мердж мастера в ветку с changelog
This commit is contained in:
commit
5094f4556d
53
Persistence.API/Controllers/SetpointController.cs
Normal file
53
Persistence.API/Controllers/SetpointController.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Persistence.Models;
|
||||||
|
using Persistence.Repositories;
|
||||||
|
|
||||||
|
namespace Persistence.API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class SetpointController : ControllerBase, ISetpointApi
|
||||||
|
{
|
||||||
|
private readonly ISetpointRepository setpointRepository;
|
||||||
|
|
||||||
|
public SetpointController(ISetpointRepository setpointRepository)
|
||||||
|
{
|
||||||
|
this.setpointRepository = setpointRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("current")]
|
||||||
|
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await setpointRepository.GetCurrent(setpointKeys, token);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("history")]
|
||||||
|
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetHistory([FromQuery] IEnumerable<Guid> setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("log")]
|
||||||
|
public async Task<ActionResult<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await setpointRepository.GetLog(setpointKeys, token);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<int>> Save(Guid setpointKey, object newValue, CancellationToken token)
|
||||||
|
{
|
||||||
|
// ToDo: вычитка idUser
|
||||||
|
await setpointRepository.Save(setpointKey, newValue, 0, token);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
Persistence.API/Controllers/TimestampedSetController.cs
Normal file
104
Persistence.API/Controllers/TimestampedSetController.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Persistence.Models;
|
||||||
|
using Persistence.Repositories;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Persistence.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Хранение наборов данных с отметкой времени.
|
||||||
|
/// Не оптимизировано под большие данные.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]/{idDiscriminator}")]
|
||||||
|
public class TimestampedSetController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ITimestampedSetRepository repository;
|
||||||
|
|
||||||
|
public TimestampedSetController(ITimestampedSetRepository repository)
|
||||||
|
{
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Записать новые данные
|
||||||
|
/// Предполагается что данные с одним дискриминатором имеют одинаковую структуру
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="sets"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns>кол-во затронутых записей</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<IActionResult> InsertRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable<TimestampedSetDto> sets, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await repository.InsertRange(idDiscriminator, sets, token);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получение данных с фильтрацией. Значение фильтра null - отключен
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="geTimestamp">Фильтр позднее даты</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="skip"></param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns>Фильтрованный набор данных с сортировкой по отметке времени</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<TimestampedSetDto>), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<IActionResult> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, [FromQuery]IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await repository.Get(idDiscriminator, geTimestamp, columnNames, skip, take, token);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить последние данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns>Фильтрованный набор данных с сортировкой по отметке времени</returns>
|
||||||
|
[HttpGet("last")]
|
||||||
|
[ProducesResponseType(typeof(IEnumerable<TimestampedSetDto>), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<IActionResult> GetLast(Guid idDiscriminator, [FromQuery]IEnumerable<string>? columnNames, int take, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await repository.GetLast(idDiscriminator, columnNames, take, token);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Диапазон дат за которые есть данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns>Дата первой и последней записи</returns>
|
||||||
|
[HttpGet("datesRange")]
|
||||||
|
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||||
|
public async Task<IActionResult> GetDatesRange(Guid idDiscriminator, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await repository.GetDatesRange(idDiscriminator, token);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Количество записей по указанному набору в БД. Для пагинации.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("count")]
|
||||||
|
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||||
|
public async Task<IActionResult> Count(Guid idDiscriminator, CancellationToken token)
|
||||||
|
{
|
||||||
|
var result = await repository.Count(idDiscriminator, token);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using System.Text.Json.Nodes;
|
||||||
|
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 System.Text.Json.Nodes;
|
using Persistence.Models.Configurations;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
namespace Persistence.API;
|
namespace Persistence.API;
|
||||||
|
|
||||||
@ -30,7 +32,103 @@ public static class DependencyInjection
|
|||||||
});
|
});
|
||||||
|
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" });
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" });
|
||||||
c.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme
|
|
||||||
|
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>();
|
||||||
|
if (needUseKeyCloak)
|
||||||
|
c.AddKeycloackSecurity(configuration);
|
||||||
|
else c.AddDefaultSecurity(configuration);
|
||||||
|
|
||||||
|
//var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||||
|
//var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||||
|
//var includeControllerXmlComment = true;
|
||||||
|
//options.IncludeXmlComments(xmlPath, includeControllerXmlComment);
|
||||||
|
//options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Authentication
|
||||||
|
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var needUseKeyCloak = configuration
|
||||||
|
.GetSection("NeedUseKeyCloak")
|
||||||
|
.Get<bool>();
|
||||||
|
if (needUseKeyCloak)
|
||||||
|
services.AddKeyCloakAuthentication(configuration);
|
||||||
|
else services.AddDefaultAuthentication(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.Audience = configuration["Authentication:Audience"];
|
||||||
|
options.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidIssuer = configuration["Authentication:ValidIssuer"],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = JwtParams.Issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = JwtParams.Audience,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
IssuerSigningKey = JwtParams.SecurityKey,
|
||||||
|
ValidateIssuerSigningKey = false
|
||||||
|
};
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Headers["Authorization"]
|
||||||
|
.ToString()
|
||||||
|
.Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty)
|
||||||
|
.Trim();
|
||||||
|
|
||||||
|
context.Token = accessToken;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
OnTokenValidated = context =>
|
||||||
|
{
|
||||||
|
var username = context.Principal?.Claims
|
||||||
|
.FirstOrDefault(e => e.Type == "username")?.Value;
|
||||||
|
|
||||||
|
var password = context.Principal?.Claims
|
||||||
|
.FirstOrDefault(e => e.Type == "password")?.Value;
|
||||||
|
|
||||||
|
var keyCloakUser = configuration
|
||||||
|
.GetSection(nameof(AuthUser))
|
||||||
|
.Get<AuthUser>()!;
|
||||||
|
|
||||||
|
if (username != keyCloakUser.Username || password != keyCloakUser.Password)
|
||||||
|
{
|
||||||
|
context.Fail("username or password did not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Security (Swagger)
|
||||||
|
private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
|
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
|
||||||
Name = "Authorization",
|
Name = "Authorization",
|
||||||
@ -45,7 +143,7 @@ public static class DependencyInjection
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
new OpenApiSecurityScheme
|
new OpenApiSecurityScheme
|
||||||
@ -62,27 +160,36 @@ public static class DependencyInjection
|
|||||||
new List<string>()
|
new List<string>()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
||||||
//var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
|
||||||
//var includeControllerXmlComment = true;
|
|
||||||
//c.IncludeXmlComments(xmlPath, includeControllerXmlComment);
|
|
||||||
//c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
|
private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
.AddJwtBearer(o =>
|
|
||||||
{
|
{
|
||||||
o.RequireHttpsMetadata = false;
|
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
|
||||||
o.Audience = configuration["Authentication:Audience"];
|
Name = "Authorization",
|
||||||
o.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
|
In = ParameterLocation.Header,
|
||||||
o.TokenValidationParameters = new TokenValidationParameters
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer",
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
|
||||||
{
|
{
|
||||||
ValidIssuer = configuration["Authentication:ValidIssuer"],
|
{
|
||||||
};
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
},
|
||||||
|
Scheme = "oauth2",
|
||||||
|
Name = "Bearer",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
},
|
||||||
|
new List<string>()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "http://localhost:5032"
|
"applicationUrl": "http://localhost:13616"
|
||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
|
@ -4,5 +4,6 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"NeedUseKeyCloak": false
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"DbConnection": {
|
"DbConnection": {
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"Port": 5432,
|
"Port": 5432,
|
||||||
"Username": "postgres",
|
"Username": "postgres",
|
||||||
"Password": "q"
|
"Password": "q"
|
||||||
},
|
},
|
||||||
"KeycloakTestUser": {
|
"NeedUseKeyCloak": false,
|
||||||
|
"AuthUser": {
|
||||||
"username": "myuser",
|
"username": "myuser",
|
||||||
"password": 12345,
|
"password": 12345,
|
||||||
"clientId": "webapi",
|
"clientId": "webapi",
|
||||||
|
24
Persistence.Client/Clients/ISetpointClient.cs
Normal file
24
Persistence.Client/Clients/ISetpointClient.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using Persistence.Models;
|
||||||
|
using Refit;
|
||||||
|
|
||||||
|
namespace Persistence.Client.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Интерфейс для тестирования API, предназначенного для работы с уставками
|
||||||
|
/// </summary>
|
||||||
|
public interface ISetpointClient
|
||||||
|
{
|
||||||
|
private const string BaseRoute = "/api/setpoint";
|
||||||
|
|
||||||
|
[Get($"{BaseRoute}/current")]
|
||||||
|
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
|
||||||
|
|
||||||
|
[Get($"{BaseRoute}/history")]
|
||||||
|
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, [Query] DateTimeOffset historyMoment);
|
||||||
|
|
||||||
|
[Get($"{BaseRoute}/log")]
|
||||||
|
Task<IApiResponse<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
|
||||||
|
|
||||||
|
[Post($"{BaseRoute}/")]
|
||||||
|
Task<IApiResponse> Save(Guid setpointKey, object newValue);
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
using Persistence.Models;
|
using Persistence.Models;
|
||||||
using Refit;
|
using Refit;
|
||||||
|
|
||||||
namespace Persistence.IntegrationTests.Clients;
|
namespace Persistence.Client.Clients;
|
||||||
public interface ITimeSeriesClient<TDto>
|
public interface ITimeSeriesClient<TDto>
|
||||||
where TDto : class, new()
|
where TDto : class, new()
|
||||||
{
|
{
|
62
Persistence.Client/Clients/ITimestampedSetClient.cs
Normal file
62
Persistence.Client/Clients/ITimestampedSetClient.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Persistence.Models;
|
||||||
|
using Refit;
|
||||||
|
|
||||||
|
namespace Persistence.Client.Clients;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Клиент для работы с репозиторием для хранения разных наборов данных рядов.
|
||||||
|
/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest.
|
||||||
|
/// idDiscriminator формируют клиенты и только им известно что они обозначают.
|
||||||
|
/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITimestampedSetClient
|
||||||
|
{
|
||||||
|
private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавление новых данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="sets"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Post(baseUrl)]
|
||||||
|
Task<IApiResponse<int>> InsertRange(Guid idDiscriminator, IEnumerable<TimestampedSetDto> sets);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получение данных с фильтрацией. Значение фильтра null - отключен
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="geTimestamp">Фильтр позднее даты</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="skip"></param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Get(baseUrl)]
|
||||||
|
Task<IApiResponse<IEnumerable<TimestampedSetDto>>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable<string>? columnNames, int skip, int take);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить последние данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Get($"{baseUrl}/last")]
|
||||||
|
Task<IApiResponse<IEnumerable<TimestampedSetDto>>> GetLast(Guid idDiscriminator, [Query] IEnumerable<string>? columnNames, int take);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Количество записей по указанному набору в БД. Для пагинации.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Get($"{baseUrl}/count")]
|
||||||
|
Task<IApiResponse<int>> Count(Guid idDiscriminator);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Диапазон дат за которые есть данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Get($"{baseUrl}/datesRange")]
|
||||||
|
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator);
|
||||||
|
}
|
72
Persistence.Client/Helpers/ApiTokenHelper.cs
Normal file
72
Persistence.Client/Helpers/ApiTokenHelper.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Persistence.Models.Configurations;
|
||||||
|
using RestSharp;
|
||||||
|
|
||||||
|
namespace Persistence.Client.Helpers;
|
||||||
|
public static class ApiTokenHelper
|
||||||
|
{
|
||||||
|
public static void Authorize(this HttpClient httpClient, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var authUser = configuration
|
||||||
|
.GetSection(nameof(AuthUser))
|
||||||
|
.Get<AuthUser>()!;
|
||||||
|
var needUseKeyCloak = configuration
|
||||||
|
.GetSection("NeedUseKeyCloak")
|
||||||
|
.Get<bool>()!;
|
||||||
|
var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty;
|
||||||
|
|
||||||
|
var jwtToken = needUseKeyCloak
|
||||||
|
? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl)
|
||||||
|
: authUser.CreateDefaultJwtToken();
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateDefaultJwtToken(this AuthUser authUser)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new("client_id", authUser.ClientId),
|
||||||
|
new("username", authUser.Username),
|
||||||
|
new("password", authUser.Password),
|
||||||
|
new("grant_type", authUser.GrantType)
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Issuer = JwtParams.Issuer,
|
||||||
|
Audience = JwtParams.Audience,
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Expires = DateTime.UtcNow.AddHours(1),
|
||||||
|
SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature)
|
||||||
|
};
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl)
|
||||||
|
{
|
||||||
|
var restClient = new RestClient();
|
||||||
|
|
||||||
|
var request = new RestRequest(keycloakGetTokenUrl, Method.Post);
|
||||||
|
request.AddParameter("username", authUser.Username);
|
||||||
|
request.AddParameter("password", authUser.Password);
|
||||||
|
request.AddParameter("client_id", authUser.ClientId);
|
||||||
|
request.AddParameter("grant_type", authUser.GrantType);
|
||||||
|
|
||||||
|
var keyCloackResponse = restClient.Post(request);
|
||||||
|
if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content))
|
||||||
|
{
|
||||||
|
var token = JsonSerializer.Deserialize<JwtToken>(keyCloackResponse.Content)!;
|
||||||
|
return token.AccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
}
|
25
Persistence.Client/Persistence.Client.csproj
Normal file
25
Persistence.Client/Persistence.Client.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
|
||||||
|
<PackageReference Include="Refit" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
|
||||||
|
<PackageReference Include="RestSharp" Version="112.1.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Persistence\Persistence.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Models\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
32
Persistence.Client/PersistenceClientFactory.cs
Normal file
32
Persistence.Client/PersistenceClientFactory.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Persistence.Client.Helpers;
|
||||||
|
using Refit;
|
||||||
|
|
||||||
|
namespace Persistence.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Фабрика клиентов для доступа к Persistence - сервису
|
||||||
|
/// </summary>
|
||||||
|
public class PersistenceClientFactory
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions));
|
||||||
|
private HttpClient httpClient;
|
||||||
|
public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
this.httpClient = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
httpClient.Authorize(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetClient<T>()
|
||||||
|
{
|
||||||
|
return RestService.For<T>(httpClient, RefitSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.Designer.cs
generated
Normal file
146
Persistence.Database.Postgres/Migrations/20241118052225_SetpointMigration.Designer.cs
generated
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Persistence.Database.Model;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Persistence.Database.Postgres.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PersistenceDbContext))]
|
||||||
|
[Migration("20241118052225_SetpointMigration")]
|
||||||
|
partial class SetpointMigration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.UseCollation("Russian_Russia.1251")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<double?>("AxialLoad")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("axialLoad");
|
||||||
|
|
||||||
|
b.Property<double?>("BitDepth")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("bitDepth");
|
||||||
|
|
||||||
|
b.Property<double?>("BlockPosition")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("blockPosition");
|
||||||
|
|
||||||
|
b.Property<double?>("BlockSpeed")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("blockSpeed");
|
||||||
|
|
||||||
|
b.Property<double?>("Flow")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("flow");
|
||||||
|
|
||||||
|
b.Property<double?>("HookWeight")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("hookWeight");
|
||||||
|
|
||||||
|
b.Property<int>("IdFeedRegulator")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("idFeedRegulator");
|
||||||
|
|
||||||
|
b.Property<int?>("Mode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("mode");
|
||||||
|
|
||||||
|
b.Property<double?>("Mse")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("mse");
|
||||||
|
|
||||||
|
b.Property<short>("MseState")
|
||||||
|
.HasColumnType("smallint")
|
||||||
|
.HasColumnName("mseState");
|
||||||
|
|
||||||
|
b.Property<double?>("Pressure")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("pressure");
|
||||||
|
|
||||||
|
b.Property<double?>("Pump0Flow")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("pump0Flow");
|
||||||
|
|
||||||
|
b.Property<double?>("Pump1Flow")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("pump1Flow");
|
||||||
|
|
||||||
|
b.Property<double?>("Pump2Flow")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("pump2Flow");
|
||||||
|
|
||||||
|
b.Property<double?>("RotorSpeed")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("rotorSpeed");
|
||||||
|
|
||||||
|
b.Property<double?>("RotorTorque")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("rotorTorque");
|
||||||
|
|
||||||
|
b.Property<int>("TimeStamp")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("timestamp");
|
||||||
|
|
||||||
|
b.Property<string>("User")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("user");
|
||||||
|
|
||||||
|
b.Property<double?>("WellDepth")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("wellDepth");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("DataSaub");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Persistence.Database.Model.Setpoint", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Key")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasComment("Ключ");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("Дата изменения уставки");
|
||||||
|
|
||||||
|
b.Property<int>("IdUser")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("Id автора последнего изменения");
|
||||||
|
|
||||||
|
b.Property<object>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasComment("Значение уставки");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Created");
|
||||||
|
|
||||||
|
b.ToTable("Setpoint");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Persistence.Database.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SetpointMigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Setpoint",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ"),
|
||||||
|
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата изменения уставки"),
|
||||||
|
Value = table.Column<object>(type: "jsonb", nullable: false, comment: "Значение уставки"),
|
||||||
|
IdUser = table.Column<int>(type: "integer", nullable: false, comment: "Id автора последнего изменения")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Setpoint", x => new { x.Key, x.Created });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Setpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,6 +160,30 @@ namespace Persistence.Database.Postgres.Migrations
|
|||||||
|
|
||||||
b.ToTable("DataSaub");
|
b.ToTable("DataSaub");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Persistence.Database.Model.Setpoint", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Key")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasComment("Ключ");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("Дата изменения уставки");
|
||||||
|
|
||||||
|
b.Property<int>("IdUser")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("Id автора последнего изменения");
|
||||||
|
|
||||||
|
b.Property<object>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasComment("Значение уставки");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Created");
|
||||||
|
|
||||||
|
b.ToTable("Setpoint");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
using Persistence.Database.Entity;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
|
||||||
namespace Persistence.Database.Model;
|
namespace Persistence.Database.Model;
|
||||||
public partial class PersistenceDbContext : DbContext, IPersistenceDbContext
|
public partial class PersistenceDbContext : DbContext
|
||||||
{
|
{
|
||||||
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
|
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
|
||||||
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
|
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
|
||||||
|
|
||||||
|
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
|
||||||
|
|
||||||
|
public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
|
||||||
|
|
||||||
public PersistenceDbContext()
|
public PersistenceDbContext()
|
||||||
: base()
|
: base()
|
||||||
{
|
{
|
||||||
@ -31,7 +37,9 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext
|
|||||||
{
|
{
|
||||||
modelBuilder.HasPostgresExtension("adminpack")
|
modelBuilder.HasPostgresExtension("adminpack")
|
||||||
.HasAnnotation("Relational:Collation", "Russian_Russia.1251");
|
.HasAnnotation("Relational:Collation", "Russian_Russia.1251");
|
||||||
|
|
||||||
|
modelBuilder.Entity<TimestampedSet>()
|
||||||
|
.Property(e => e.Set)
|
||||||
|
.HasJsonConversion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
41
Persistence.Database/EFExtensions.cs
Normal file
41
Persistence.Database/EFExtensions.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Persistence.Database;
|
||||||
|
|
||||||
|
public static class EFExtensions
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions jsonSerializerOptions = new()
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
WriteIndented = true,
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<TProperty> HasJsonConversion<TProperty>(
|
||||||
|
this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<TProperty> builder)
|
||||||
|
=> HasJsonConversion(builder, jsonSerializerOptions);
|
||||||
|
|
||||||
|
public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<TProperty> HasJsonConversion<TProperty>(
|
||||||
|
this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<TProperty> builder,
|
||||||
|
JsonSerializerOptions jsonSerializerOptions)
|
||||||
|
{
|
||||||
|
builder.HasConversion(
|
||||||
|
s => JsonSerializer.Serialize(s, jsonSerializerOptions),
|
||||||
|
s => JsonSerializer.Deserialize<TProperty>(s, jsonSerializerOptions)!);
|
||||||
|
|
||||||
|
ValueComparer<TProperty> valueComparer = new(
|
||||||
|
(a, b) =>
|
||||||
|
(a != null) && (b != null)
|
||||||
|
? a.GetHashCode() == b.GetHashCode()
|
||||||
|
: (a == null) && (b == null),
|
||||||
|
i => (i == null) ? -1 : i.GetHashCode(),
|
||||||
|
i => i);
|
||||||
|
|
||||||
|
builder.Metadata.SetValueComparer(valueComparer);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
21
Persistence.Database/Entity/Setpoint.cs
Normal file
21
Persistence.Database/Entity/Setpoint.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Persistence.Database.Model
|
||||||
|
{
|
||||||
|
[PrimaryKey(nameof(Key), nameof(Created))]
|
||||||
|
public class Setpoint
|
||||||
|
{
|
||||||
|
[Comment("Ключ")]
|
||||||
|
public Guid Key { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb"), Comment("Значение уставки")]
|
||||||
|
public required object Value { get; set; }
|
||||||
|
|
||||||
|
[Comment("Дата создания уставки")]
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
|
||||||
|
[Comment("Id автора последнего изменения")]
|
||||||
|
public int IdUser { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
Persistence.Database/Entity/TimestampedSet.cs
Normal file
11
Persistence.Database/Entity/TimestampedSet.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Persistence.Database.Entity;
|
||||||
|
|
||||||
|
[Comment("Общая таблица данных временных рядов")]
|
||||||
|
[PrimaryKey(nameof(IdDiscriminator), nameof(Timestamp))]
|
||||||
|
public record TimestampedSet(
|
||||||
|
[property: Comment("Дискриминатор ссылка на тип сохраняемых данных")] Guid IdDiscriminator,
|
||||||
|
[property: Comment("Отметка времени, строго в UTC")] DateTimeOffset Timestamp,
|
||||||
|
[property: Column(TypeName = "jsonb"), Comment("Набор сохраняемых данных")] IDictionary<string, object> Set);
|
@ -1,9 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Persistence.Database.Model;
|
|
||||||
|
|
||||||
namespace Persistence.Database;
|
|
||||||
public interface IPersistenceDbContext : IDisposable
|
|
||||||
{
|
|
||||||
DbSet<DataSaub> DataSaub { get; }
|
|
||||||
DbSet<ChangeLog> ChangeLog { get; }
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
namespace Persistence.IntegrationTests;
|
|
||||||
public static class ApiTokenHelper
|
|
||||||
{
|
|
||||||
//public static string GetAdminUserToken()
|
|
||||||
//{
|
|
||||||
// var user = new User()
|
|
||||||
// {
|
|
||||||
// Id = 1,
|
|
||||||
// IdCompany = 1,
|
|
||||||
// Login = "test_user"
|
|
||||||
// };
|
|
||||||
// var roles = new[] { "root" };
|
|
||||||
|
|
||||||
// return CreateToken(user, roles);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private static string CreateToken(User user, IEnumerable<string> roles)
|
|
||||||
//{
|
|
||||||
// var claims = new List<Claim>
|
|
||||||
// {
|
|
||||||
// new("id", user.Id.ToString()),
|
|
||||||
// new(ClaimsIdentity.DefaultNameClaimType, user.Login),
|
|
||||||
// new("idCompany", user.IdCompany.ToString()),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
|
||||||
|
|
||||||
// const string secret = "супер секретный ключ для шифрования";
|
|
||||||
|
|
||||||
// var key = Encoding.ASCII.GetBytes(secret);
|
|
||||||
// var tokenDescriptor = new SecurityTokenDescriptor
|
|
||||||
// {
|
|
||||||
// Issuer = "a",
|
|
||||||
// Audience = "a",
|
|
||||||
// Subject = new ClaimsIdentity(claims),
|
|
||||||
// Expires = DateTime.UtcNow.AddHours(1),
|
|
||||||
// SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
|
||||||
// };
|
|
||||||
// var tokenHandler = new JwtSecurityTokenHandler();
|
|
||||||
// var token = tokenHandler.CreateToken(tokenDescriptor);
|
|
||||||
// return tokenHandler.WriteToken(token);
|
|
||||||
//}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using Persistence.Database.Model;
|
using Persistence.Client;
|
||||||
|
using Persistence.Database.Model;
|
||||||
using Persistence.Repository.Data;
|
using Persistence.Repository.Data;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Persistence.Client;
|
||||||
|
using Persistence.Client.Clients;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Persistence.IntegrationTests.Controllers
|
||||||
|
{
|
||||||
|
public class SetpointControllerTest : BaseIntegrationTest
|
||||||
|
{
|
||||||
|
private ISetpointClient setpointClient;
|
||||||
|
private class TestObject
|
||||||
|
{
|
||||||
|
public string? value1 { get; set; }
|
||||||
|
public int? value2 { get; set; }
|
||||||
|
}
|
||||||
|
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
|
||||||
|
{
|
||||||
|
var scope = factory.Services.CreateScope();
|
||||||
|
var persistenceClientFactory = scope.ServiceProvider
|
||||||
|
.GetRequiredService<PersistenceClientFactory>();
|
||||||
|
|
||||||
|
setpointClient = persistenceClientFactory.GetClient<ISetpointClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCurrent_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKeys = new List<Guid>()
|
||||||
|
{
|
||||||
|
Guid.NewGuid(),
|
||||||
|
Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetCurrent(setpointKeys);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.Empty(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCurrent_AfterSave_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKey = await Save();
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetCurrent([setpointKey]);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.NotEmpty(response.Content);
|
||||||
|
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetHistory_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKeys = new List<Guid>()
|
||||||
|
{
|
||||||
|
Guid.NewGuid(),
|
||||||
|
Guid.NewGuid()
|
||||||
|
};
|
||||||
|
var historyMoment = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetHistory(setpointKeys, historyMoment);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.Empty(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetHistory_AfterSave_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKey = await Save();
|
||||||
|
var historyMoment = DateTimeOffset.UtcNow;
|
||||||
|
historyMoment = historyMoment.AddDays(1);
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetHistory([setpointKey], historyMoment);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.NotEmpty(response.Content);
|
||||||
|
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLog_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKeys = new List<Guid>()
|
||||||
|
{
|
||||||
|
Guid.NewGuid(),
|
||||||
|
Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetLog(setpointKeys);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.Empty(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLog_AfterSave_returns_success()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKey = await Save();
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.GetLog([setpointKey]);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
Assert.NotEmpty(response.Content);
|
||||||
|
Assert.Equal(setpointKey, response.Content.FirstOrDefault().Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Save_returns_success()
|
||||||
|
{
|
||||||
|
await Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Guid> Save()
|
||||||
|
{
|
||||||
|
//arrange
|
||||||
|
var setpointKey = Guid.NewGuid();
|
||||||
|
var setpointValue = new TestObject()
|
||||||
|
{
|
||||||
|
value1 = "1",
|
||||||
|
value2 = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
//act
|
||||||
|
var response = await setpointClient.Save(setpointKey, setpointValue);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
return setpointKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
using Mapster;
|
|
||||||
using Persistence.Database.Model;
|
|
||||||
using Persistence.IntegrationTests.Clients;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Mapster;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Persistence.Client;
|
||||||
|
using Persistence.Client.Clients;
|
||||||
|
using Persistence.Database.Model;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Persistence.IntegrationTests.Controllers;
|
namespace Persistence.IntegrationTests.Controllers;
|
||||||
@ -9,16 +11,17 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
|
|||||||
where TEntity : class, ITimestampedData, new()
|
where TEntity : class, ITimestampedData, new()
|
||||||
where TDto : class, new()
|
where TDto : class, new()
|
||||||
{
|
{
|
||||||
private ITimeSeriesClient<TDto> client;
|
private ITimeSeriesClient<TDto> timeSeriesClient;
|
||||||
|
|
||||||
public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory)
|
public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory)
|
||||||
{
|
{
|
||||||
dbContext.CleanupDbSet<TEntity>();
|
dbContext.CleanupDbSet<TEntity>();
|
||||||
|
|
||||||
Task.Run(async () =>
|
var scope = factory.Services.CreateScope();
|
||||||
{
|
var persistenceClientFactory = scope.ServiceProvider
|
||||||
client = await factory.GetAuthorizedHttpClient<ITimeSeriesClient<TDto>>(string.Empty);
|
.GetRequiredService<PersistenceClientFactory>();
|
||||||
}).Wait();
|
|
||||||
|
timeSeriesClient = persistenceClientFactory.GetClient<ITimeSeriesClient<TDto>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InsertRangeSuccess(TDto dto)
|
public async Task InsertRangeSuccess(TDto dto)
|
||||||
@ -27,7 +30,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
|
|||||||
var expected = dto.Adapt<TDto>();
|
var expected = dto.Adapt<TDto>();
|
||||||
|
|
||||||
//act
|
//act
|
||||||
var response = await client.InsertRange(new TDto[] { expected });
|
var response = await timeSeriesClient.InsertRange(new TDto[] { expected });
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
@ -43,7 +46,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
|
|||||||
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
var response = await client.Get(beginDate, endDate);
|
var response = await timeSeriesClient.Get(beginDate, endDate);
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
@ -65,7 +68,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
|
|||||||
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
var response = await client.GetDatesRange();
|
var response = await timeSeriesClient.GetDatesRange();
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
@ -95,7 +98,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
|
|||||||
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
|
|
||||||
var response = await client.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount);
|
var response = await timeSeriesClient.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount);
|
||||||
|
|
||||||
//assert
|
//assert
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
@ -0,0 +1,222 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Persistence.Client;
|
||||||
|
using Persistence.Client.Clients;
|
||||||
|
using Persistence.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Persistence.IntegrationTests.Controllers;
|
||||||
|
public class TimestampedSetControllerTest : BaseIntegrationTest
|
||||||
|
{
|
||||||
|
private readonly ITimestampedSetClient client;
|
||||||
|
|
||||||
|
public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory)
|
||||||
|
{
|
||||||
|
var persistenceClientFactory = scope.ServiceProvider
|
||||||
|
.GetRequiredService<PersistenceClientFactory>();
|
||||||
|
|
||||||
|
client = persistenceClientFactory.GetClient<ITimestampedSetClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InsertRange()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(10, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal(testSets.Count(), response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_without_filter()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Get(idDiscriminator, null, null, 0, int.MaxValue);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(count, items.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_with_filter_props()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
string[] props = ["A"];
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Get(idDiscriminator, null, props, 0, int.MaxValue);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(count, items.Count());
|
||||||
|
foreach ( var item in items )
|
||||||
|
{
|
||||||
|
Assert.Single(item.Set);
|
||||||
|
var kv = item.Set.First();
|
||||||
|
Assert.Equal("A", kv.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_geDate()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
var dateMin = DateTimeOffset.Now;
|
||||||
|
var dateMax = DateTimeOffset.Now.AddSeconds(count);
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
var tail = testSets.OrderBy(t => t.Timestamp).Skip(count / 2).Take(int.MaxValue);
|
||||||
|
var geDate = tail.First().Timestamp;
|
||||||
|
var tolerance = TimeSpan.FromSeconds(1);
|
||||||
|
var expectedCount = tail.Count();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Get(idDiscriminator, geDate, null, 0, int.MaxValue);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(expectedCount, items.Count());
|
||||||
|
var minDate = items.Min(t => t.Timestamp);
|
||||||
|
Assert.Equal(geDate, geDate, tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_with_skip_take()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
var expectedCount = count / 2;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Get(idDiscriminator, null, null, 2, expectedCount);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(expectedCount, items.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_with_big_skip_take()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
var expectedCount = 1;
|
||||||
|
int count = 10 + expectedCount;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Get(idDiscriminator, null, null, count - expectedCount, count);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(expectedCount, items.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLast()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
var expectedCount = 8;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.GetLast(idDiscriminator, null, expectedCount);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var items = response.Content!;
|
||||||
|
Assert.Equal(expectedCount, items.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDatesRange()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 10;
|
||||||
|
var dateMin = DateTimeOffset.Now;
|
||||||
|
var dateMax = DateTimeOffset.Now.AddSeconds(count-1);
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
var tolerance = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.GetDatesRange(idDiscriminator);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.NotNull(response.Content);
|
||||||
|
var range = response.Content!;
|
||||||
|
Assert.Equal(dateMin, range.From, tolerance);
|
||||||
|
Assert.Equal(dateMax, range.To, tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Count()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Guid idDiscriminator = Guid.NewGuid();
|
||||||
|
int count = 144;
|
||||||
|
IEnumerable<TimestampedSetDto> testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7)));
|
||||||
|
var insertResponse = await client.InsertRange(idDiscriminator, testSets);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.Count(idDiscriminator);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal(count, response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TimestampedSetDto> Generate(int n, DateTimeOffset from)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
yield return new TimestampedSetDto
|
||||||
|
(
|
||||||
|
from.AddSeconds(i),
|
||||||
|
new Dictionary<string, object>{
|
||||||
|
{"A", i },
|
||||||
|
{"B", i * 1.1 },
|
||||||
|
{"C", $"Any{i}" },
|
||||||
|
{"D", DateTimeOffset.Now},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Persistence.IntegrationTests;
|
|
||||||
public class JwtToken
|
|
||||||
{
|
|
||||||
[JsonPropertyName("access_token")]
|
|
||||||
public required string AccessToken { get; set; }
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
namespace Persistence.IntegrationTests;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// настройки credentials для пользователя в KeyCloak
|
|
||||||
/// </summary>
|
|
||||||
public class KeyCloakUser
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string Username { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string Password { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string ClientId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public required string GrantType { get; set; }
|
|
||||||
}
|
|
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Persistence.API\Persistence.API.csproj" />
|
<ProjectReference Include="..\Persistence.API\Persistence.API.csproj" />
|
||||||
|
<ProjectReference Include="..\Persistence.Client\Persistence.Client.csproj" />
|
||||||
<ProjectReference Include="..\Persistence.Database.Postgres\Persistence.Database.Postgres.csproj" />
|
<ProjectReference Include="..\Persistence.Database.Postgres\Persistence.Database.Postgres.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
19
Persistence.IntegrationTests/TestHttpClientFactory.cs
Normal file
19
Persistence.IntegrationTests/TestHttpClientFactory.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace Persistence.IntegrationTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Фабрика HTTP клиентов для интеграционных тестов
|
||||||
|
/// </summary>
|
||||||
|
public class TestHttpClientFactory : IHttpClientFactory
|
||||||
|
{
|
||||||
|
private readonly WebAppFactoryFixture factory;
|
||||||
|
|
||||||
|
public TestHttpClientFactory(WebAppFactoryFixture factory)
|
||||||
|
{
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
public HttpClient CreateClient(string name)
|
||||||
|
{
|
||||||
|
return factory.CreateClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,64 +1,53 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Persistence.API;
|
using Persistence.API;
|
||||||
|
using Persistence.Database;
|
||||||
|
using Persistence.Client;
|
||||||
using Persistence.Database.Model;
|
using Persistence.Database.Model;
|
||||||
using Persistence.Database.Postgres;
|
using Persistence.Database.Postgres;
|
||||||
using Refit;
|
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Persistence.IntegrationTests;
|
namespace Persistence.IntegrationTests;
|
||||||
public class WebAppFactoryFixture : WebApplicationFactory<Startup>
|
public class WebAppFactoryFixture : WebApplicationFactory<Startup>
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
private string connectionString = string.Empty;
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
//Converters = { new ValidationResultConverter() }
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions));
|
|
||||||
|
|
||||||
private readonly string connectionString;
|
|
||||||
private readonly KeyCloakUser keycloakTestUser;
|
|
||||||
public readonly string KeycloakGetTokenUrl;
|
|
||||||
|
|
||||||
public WebAppFactoryFixture()
|
|
||||||
{
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.AddJsonFile("appsettings.Tests.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var dbConnection = configuration.GetSection("DbConnection").Get<DbConnection>()!;
|
|
||||||
connectionString = dbConnection.GetConnectionString();
|
|
||||||
|
|
||||||
keycloakTestUser = configuration.GetSection("KeycloakTestUser").Get<KeyCloakUser>()!;
|
|
||||||
|
|
||||||
KeycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Value!;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.AddJsonFile("appsettings.Tests.json");
|
||||||
|
|
||||||
|
var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
|
||||||
|
connectionString = dbConnection.GetConnectionString();
|
||||||
|
});
|
||||||
|
|
||||||
builder.ConfigureServices(services =>
|
builder.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
|
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
|
||||||
|
|
||||||
if (descriptor != null)
|
if (descriptor != null)
|
||||||
services.Remove(descriptor);
|
services.Remove(descriptor);
|
||||||
|
|
||||||
services.AddDbContext<PersistenceDbContext>(options =>
|
services.AddDbContext<PersistenceDbContext>(options =>
|
||||||
options.UseNpgsql(connectionString));
|
options.UseNpgsql(connectionString));
|
||||||
|
|
||||||
|
services.RemoveAll<IHttpClientFactory>();
|
||||||
|
services.AddSingleton<IHttpClientFactory>(provider =>
|
||||||
|
{
|
||||||
|
return new TestHttpClientFactory(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<PersistenceClientFactory>();
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var scopedServices = scope.ServiceProvider;
|
var scopedServices = scope.ServiceProvider;
|
||||||
var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
|
|
||||||
|
|
||||||
|
var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
|
||||||
dbContext.Database.EnsureCreatedAndMigrated();
|
dbContext.Database.EnsureCreatedAndMigrated();
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
});
|
});
|
||||||
@ -73,57 +62,4 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
|
|||||||
|
|
||||||
await dbContext.Database.EnsureDeletedAsync();
|
await dbContext.Database.EnsureDeletedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetHttpClient<T>(string uriSuffix)
|
|
||||||
{
|
|
||||||
var httpClient = CreateClient();
|
|
||||||
if (string.IsNullOrEmpty(uriSuffix))
|
|
||||||
return RestService.For<T>(httpClient, RefitSettings);
|
|
||||||
|
|
||||||
if (httpClient.BaseAddress is not null)
|
|
||||||
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
|
|
||||||
|
|
||||||
return RestService.For<T>(httpClient, RefitSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> GetAuthorizedHttpClient<T>(string uriSuffix)
|
|
||||||
{
|
|
||||||
var httpClient = await GetAuthorizedHttpClient();
|
|
||||||
if (string.IsNullOrEmpty(uriSuffix))
|
|
||||||
return RestService.For<T>(httpClient, RefitSettings);
|
|
||||||
|
|
||||||
if (httpClient.BaseAddress is not null)
|
|
||||||
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
|
|
||||||
|
|
||||||
return RestService.For<T>(httpClient, RefitSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<HttpClient> GetAuthorizedHttpClient()
|
|
||||||
{
|
|
||||||
var httpClient = CreateClient();
|
|
||||||
var token = await GetTokenAsync();
|
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
||||||
|
|
||||||
return httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> GetTokenAsync()
|
|
||||||
{
|
|
||||||
var restClient = new RestClient();
|
|
||||||
|
|
||||||
var request = new RestRequest(KeycloakGetTokenUrl, Method.Post);
|
|
||||||
request.AddParameter("username", keycloakTestUser.Username);
|
|
||||||
request.AddParameter("password", keycloakTestUser.Password);
|
|
||||||
request.AddParameter("client_id", keycloakTestUser.ClientId);
|
|
||||||
request.AddParameter("grant_type", keycloakTestUser.GrantType);
|
|
||||||
|
|
||||||
var keyCloackResponse = await restClient.PostAsync(request);
|
|
||||||
if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content))
|
|
||||||
{
|
|
||||||
var token = JsonSerializer.Deserialize<JwtToken>(keyCloackResponse.Content)!;
|
|
||||||
return token.AccessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
28
Persistence.Repository/Data/SetpointDto.cs
Normal file
28
Persistence.Repository/Data/SetpointDto.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace Persistence.Repository.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Модель для работы с уставкой
|
||||||
|
/// </summary>
|
||||||
|
public class SetpointDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Идентификатор уставки
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Значение уставки
|
||||||
|
/// </summary>
|
||||||
|
public required object Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дата сохранения уставки
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset Edit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ пользователя
|
||||||
|
/// </summary>
|
||||||
|
public int IdUser { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Persistence.Database.Model;
|
using Persistence.Database.Model;
|
||||||
using Persistence.Models;
|
using Persistence.Models;
|
||||||
using Persistence.Repositories;
|
using Persistence.Repositories;
|
||||||
@ -16,8 +16,11 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
MapsterSetup();
|
MapsterSetup();
|
||||||
|
|
||||||
|
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
|
||||||
|
services.AddTransient<ISetpointRepository, SetpointRepository>();
|
||||||
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
|
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
|
||||||
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
|
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
|
||||||
|
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
73
Persistence.Repository/Repositories/SetpointRepository.cs
Normal file
73
Persistence.Repository/Repositories/SetpointRepository.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
using Mapster;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Persistence.Database.Model;
|
||||||
|
using Persistence.Models;
|
||||||
|
using Persistence.Repositories;
|
||||||
|
|
||||||
|
namespace Persistence.Repository.Repositories
|
||||||
|
{
|
||||||
|
public class SetpointRepository : ISetpointRepository
|
||||||
|
{
|
||||||
|
private DbContext db;
|
||||||
|
public SetpointRepository(DbContext db)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||||
|
{
|
||||||
|
var query = GetQueryReadOnly();
|
||||||
|
var entities = await query
|
||||||
|
.Where(e => setpointKeys.Contains(e.Key))
|
||||||
|
.ToArrayAsync(token);
|
||||||
|
var dtos = entities.Select(e => e.Adapt<SetpointValueDto>());
|
||||||
|
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
|
||||||
|
{
|
||||||
|
var query = GetQueryReadOnly();
|
||||||
|
var entities = await query
|
||||||
|
.Where(e => setpointKeys.Contains(e.Key))
|
||||||
|
.ToArrayAsync(token);
|
||||||
|
var filteredEntities = entities
|
||||||
|
.GroupBy(e => e.Key)
|
||||||
|
.Select(e => e.OrderBy(o => o.Created))
|
||||||
|
.Select(e => e.Where(e => e.Created <= historyMoment).Last());
|
||||||
|
var dtos = filteredEntities
|
||||||
|
.Select(e => e.Adapt<SetpointValueDto>());
|
||||||
|
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||||
|
{
|
||||||
|
var query = GetQueryReadOnly();
|
||||||
|
var entities = await query
|
||||||
|
.Where(e => setpointKeys.Contains(e.Key))
|
||||||
|
.ToArrayAsync(token);
|
||||||
|
var dtos = entities
|
||||||
|
.GroupBy(e => e.Key)
|
||||||
|
.ToDictionary(e => e.Key, v => v.Select(z => z.Adapt<SetpointLogDto>()));
|
||||||
|
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token)
|
||||||
|
{
|
||||||
|
var entity = new Setpoint()
|
||||||
|
{
|
||||||
|
Key = setpointKey,
|
||||||
|
Value = newValue,
|
||||||
|
IdUser = idUser,
|
||||||
|
Created = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.Set<Setpoint>().AddAsync(entity, token);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
Persistence.Repository/Repositories/TimestampedSetRepository.cs
Normal file
121
Persistence.Repository/Repositories/TimestampedSetRepository.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Persistence.Database.Entity;
|
||||||
|
using Persistence.Models;
|
||||||
|
using Persistence.Repositories;
|
||||||
|
|
||||||
|
namespace Persistence.Repository.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий для хранения разных наборов данных временных рядов.
|
||||||
|
/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest.
|
||||||
|
/// idDiscriminator формируют клиенты и только им известно что они обозначают.
|
||||||
|
/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено.
|
||||||
|
/// </summary>
|
||||||
|
public class TimestampedSetRepository : ITimestampedSetRepository
|
||||||
|
{
|
||||||
|
private readonly DbContext db;
|
||||||
|
|
||||||
|
public TimestampedSetRepository(DbContext db)
|
||||||
|
{
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> InsertRange(Guid idDiscriminator, IEnumerable<TimestampedSetDto> sets, CancellationToken token)
|
||||||
|
{
|
||||||
|
var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set));
|
||||||
|
var dbSet = db.Set<TimestampedSet>();
|
||||||
|
dbSet.AddRange(entities);
|
||||||
|
return db.SaveChangesAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TimestampedSetDto>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token)
|
||||||
|
{
|
||||||
|
var dbSet = db.Set<TimestampedSet>();
|
||||||
|
var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);
|
||||||
|
|
||||||
|
if (geTimestamp.HasValue)
|
||||||
|
query = ApplyGeTimestamp(query, geTimestamp.Value);
|
||||||
|
|
||||||
|
query = query
|
||||||
|
.OrderBy(item => item.Timestamp)
|
||||||
|
.Skip(skip)
|
||||||
|
.Take(take);
|
||||||
|
|
||||||
|
var data = await Materialize(query, token);
|
||||||
|
|
||||||
|
if (columnNames is not null && columnNames.Any())
|
||||||
|
data = ReduceSetColumnsByNames(data, columnNames);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TimestampedSetDto>> GetLast(Guid idDiscriminator, IEnumerable<string>? columnNames, int take, CancellationToken token)
|
||||||
|
{
|
||||||
|
var dbSet = db.Set<TimestampedSet>();
|
||||||
|
var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);
|
||||||
|
|
||||||
|
query = query.OrderByDescending(entity => entity.Timestamp)
|
||||||
|
.Take(take)
|
||||||
|
.OrderBy(entity => entity.Timestamp);
|
||||||
|
|
||||||
|
var data = await Materialize(query, token);
|
||||||
|
|
||||||
|
if (columnNames is not null && columnNames.Any())
|
||||||
|
data = ReduceSetColumnsByNames(data, columnNames);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> Count(Guid idDiscriminator, CancellationToken token)
|
||||||
|
{
|
||||||
|
var dbSet = db.Set<TimestampedSet>();
|
||||||
|
var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator);
|
||||||
|
return query.CountAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
|
||||||
|
{
|
||||||
|
var query = db.Set<TimestampedSet>()
|
||||||
|
.GroupBy(entity => entity.IdDiscriminator)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
Min = group.Min(entity => entity.Timestamp),
|
||||||
|
Max = group.Max(entity => entity.Timestamp),
|
||||||
|
});
|
||||||
|
|
||||||
|
var item = await query.FirstOrDefaultAsync(token);
|
||||||
|
if (item is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new DatesRangeDto
|
||||||
|
{
|
||||||
|
From = item.Min,
|
||||||
|
To = item.Max,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IEnumerable<TimestampedSetDto>> Materialize(IQueryable<TimestampedSet> query, CancellationToken token)
|
||||||
|
{
|
||||||
|
var dtoQuery = query.Select(entity => new TimestampedSetDto(entity.Timestamp, entity.Set));
|
||||||
|
var dtos = await dtoQuery.ToArrayAsync(token);
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<TimestampedSet> ApplyGeTimestamp(IQueryable<TimestampedSet> query, DateTimeOffset geTimestamp)
|
||||||
|
{
|
||||||
|
var geTimestampUtc = geTimestamp.ToUniversalTime();
|
||||||
|
return query.Where(entity => entity.Timestamp >= geTimestampUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<TimestampedSetDto> ReduceSetColumnsByNames(IEnumerable<TimestampedSetDto> query, IEnumerable<string> columnNames)
|
||||||
|
{
|
||||||
|
var newQuery = query
|
||||||
|
.Select(entity => new TimestampedSetDto(
|
||||||
|
entity.Timestamp,
|
||||||
|
entity.Set
|
||||||
|
.Where(prop => columnNames.Contains(prop.Key))
|
||||||
|
.ToDictionary(prop => prop.Key, prop => prop.Value)
|
||||||
|
));
|
||||||
|
return newQuery;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Per
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence.Database.Postgres", "Persistence.Database.Postgres\Persistence.Database.Postgres.csproj", "{CC284D27-162D-490C-B6CF-74D666B7C5F3}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database.Postgres", "Persistence.Database.Postgres\Persistence.Database.Postgres.csproj", "{CC284D27-162D-490C-B6CF-74D666B7C5F3}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence.Client", "Persistence.Client\Persistence.Client.csproj", "{84B68660-48E6-4974-A4E5-517552D9DE23}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -45,6 +47,10 @@ Global
|
|||||||
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{84B68660-48E6-4974-A4E5-517552D9DE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{84B68660-48E6-4974-A4E5-517552D9DE23}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{84B68660-48E6-4974-A4E5-517552D9DE23}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{84B68660-48E6-4974-A4E5-517552D9DE23}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
12
Persistence/Models/Configurations/AuthUser.cs
Normal file
12
Persistence/Models/Configurations/AuthUser.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Persistence.Models.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настройки credentials для авторизации
|
||||||
|
/// </summary>
|
||||||
|
public class AuthUser
|
||||||
|
{
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string Password { get; set; }
|
||||||
|
public required string ClientId { get; set; }
|
||||||
|
public required string GrantType { get; set; }
|
||||||
|
}
|
18
Persistence/Models/Configurations/JwtParams.cs
Normal file
18
Persistence/Models/Configurations/JwtParams.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Persistence.Models.Configurations
|
||||||
|
{
|
||||||
|
public static class JwtParams
|
||||||
|
{
|
||||||
|
private static readonly string KeyValue = "супер секретный ключ для шифрования";
|
||||||
|
public static SymmetricSecurityKey SecurityKey
|
||||||
|
{
|
||||||
|
get { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KeyValue)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly string Issuer = "a";
|
||||||
|
|
||||||
|
public static readonly string Audience = "a";
|
||||||
|
}
|
||||||
|
}
|
10
Persistence/Models/Configurations/JwtToken.cs
Normal file
10
Persistence/Models/Configurations/JwtToken.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Persistence.Models.Configurations
|
||||||
|
{
|
||||||
|
public class JwtToken
|
||||||
|
{
|
||||||
|
[JsonPropertyName("access_token")]
|
||||||
|
public required string AccessToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace Persistence.Models;
|
namespace Persistence.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Модель для описания лога уставки
|
/// Модель для описания лога уставки
|
||||||
@ -8,7 +8,7 @@ public class SetpointLogDto : SetpointValueDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Дата сохранения уставки
|
/// Дата сохранения уставки
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset DateEdit { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ключ пользователя
|
/// Ключ пользователя
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
namespace Persistence.Models;
|
namespace Persistence.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Модель для хранения значения уставки
|
/// Модель для хранения значения уставки
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SetpointValueDto
|
public class SetpointValueDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Идентификатор уставки
|
/// Идентификатор уставки
|
||||||
|
/// <summary>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public Guid Key { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Значение уставки
|
/// Значение уставки
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; set; }
|
public required object Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
Persistence/Models/TimestampedSetDto.cs
Normal file
8
Persistence/Models/TimestampedSetDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Persistence.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// набор данных с отметкой времени
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Timestamp">отметка времени</param>
|
||||||
|
/// <param name="Set">набор данных</param>
|
||||||
|
public record TimestampedSetDto(DateTimeOffset Timestamp, IDictionary<string, object> Set);
|
@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Persistence.Models;
|
using Persistence.Models;
|
||||||
|
|
||||||
namespace Persistence.Repositories;
|
namespace Persistence.Repositories;
|
||||||
|
|
||||||
@ -7,23 +7,30 @@ namespace Persistence.Repositories;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISetpointRepository
|
public interface ISetpointRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получить значения уставок по набору ключей
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="setpointKeys"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить значения уставок за определенный момент времени
|
/// Получить значения уставок за определенный момент времени
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="setpoitKeys"></param>
|
/// <param name="setpointKeys"></param>
|
||||||
/// <param name="historyMoment">дата, на которую получаем данные</param>
|
/// <param name="historyMoment">дата, на которую получаем данные</param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpoitKeys, DateTimeOffset historyMoment, CancellationToken token);
|
Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Получить историю изменений значений уставок
|
/// Получить историю изменений значений уставок
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="setpoitKeys"></param>
|
/// <param name="setpointKeys"></param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpoitKeys, CancellationToken token);
|
Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Метод сохранения уставки
|
/// Метод сохранения уставки
|
||||||
@ -35,5 +42,5 @@ public interface ISetpointRepository
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// to do
|
/// to do
|
||||||
/// id User учесть в соответствующем методе репозитория
|
/// id User учесть в соответствующем методе репозитория
|
||||||
Task<int> Save(Guid setpointKey, int idUser, object newValue, CancellationToken token);
|
Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
59
Persistence/Repositories/ITimestampedSetRepository.cs
Normal file
59
Persistence/Repositories/ITimestampedSetRepository.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using Persistence.Models;
|
||||||
|
|
||||||
|
namespace Persistence.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Репозиторий для хранения разных наборов данных рядов.
|
||||||
|
/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest.
|
||||||
|
/// idDiscriminator формируют клиенты и только им известно что они обозначают.
|
||||||
|
/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITimestampedSetRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Количество записей по указанному набору в БД. Для пагинации.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<int> Count(Guid idDiscriminator, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получение данных с фильтрацией. Значение фильтра null - отключен
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="geTimestamp">Фильтр позднее даты</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="skip"></param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<TimestampedSetDto>> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable<string>? columnNames, int skip, int take, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Диапазон дат за которые есть данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить последние данные
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="columnNames">Фильтр свойств набора. Можно запросить только некоторые свойства из набора</param>
|
||||||
|
/// <param name="take"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IEnumerable<TimestampedSetDto>> GetLast(Guid idDiscriminator, IEnumerable<string>? columnNames, int take, CancellationToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавление новых данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idDiscriminator">Дискриминатор (идентификатор) набора</param>
|
||||||
|
/// <param name="sets"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<int> InsertRange(Guid idDiscriminator, IEnumerable<TimestampedSetDto> sets, CancellationToken token);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user