Compare commits

..

39 Commits

Author SHA1 Message Date
fdf49b91ab Merge pull request 'Add TimestampedSetRepository' (#2) from TimestampedSetRepository into master
Reviewed-on: #2
2024-11-26 12:59:05 +05:00
ngfrolov
a0f5afb923
Merge branch 'TimestampedSetRepository' of ssh://git.ddrilling.ru:2221/on.nemtina/persistence into TimestampedSetRepository 2024-11-26 11:24:58 +05:00
ngfrolov
3bb5fc4411
TimestampedSetRepository rename private ApplyPropsFilter to ReduceSetColumnsByNames;
Fix TimestampedSetController doc and response type;
ITimestampedSetClient Add doc.
2024-11-26 11:24:53 +05:00
ngfrolov
a07dbae5b8
TimestampedSetRepository rename private ApplyPropsFilter to ReduceSetColumnsByNames;
Fix TimestampedSetController doc and response type;
ITimestampedSetClient Add doc.
2024-11-26 11:24:31 +05:00
ngfrolov
2169e592e6
Добавлен ITimestampedSetClient.
Поправлены тесты.
Удален не используемый интерфейс IPersistenceDbContext.
2024-11-26 10:07:50 +05:00
ngfrolov
02def3a7d6
merge master to TimestampedSetRepository 2024-11-25 17:17:55 +05:00
f5ed62eb7d Merge pull request '#384 Авторизации + получение id пользователя в контроллерах' (#3) from PersistenceClient into master
Reviewed-on: #3
Reviewed-by: on.nemtina <on.nemtina@digitaldrilling.ru>
2024-11-25 17:09:22 +05:00
f31805a769 Merge branch 'master' into PersistenceClient 2024-11-25 17:09:08 +05:00
b79d29f08e Внести правки по результатам ревью 2024-11-25 14:29:42 +05:00
23e2f86957 Fix - SetpointControllerTest 2024-11-25 10:54:42 +05:00
61dd5110bd Merge branch 'master' into PersistenceClient 2024-11-25 10:47:33 +05:00
afe7310f73 Merge pull request 'Setpoint API' (#1) from Setpoint into master
Reviewed-on: #1
2024-11-25 10:33:35 +05:00
4fa00de88e Внести правки по результатам ревью #2 2024-11-25 10:30:37 +05:00
c496cea07a Merge branch 'master' into Setpoint 2024-11-25 10:12:20 +05:00
3806e395eb Реализовать авторизацию для Persistence.Client 2024-11-25 10:09:38 +05:00
c1e30a8834 Merge branch 'master' into TimestampedSetRepository 2024-11-25 09:41:50 +05:00
ngfrolov
bd8de9afc2
Add TimestampedSet documentation 2024-11-25 09:41:11 +05:00
ngfrolov
b75714c835
Add TimestampedSetRepository 2024-11-22 17:52:15 +05:00
f6648b812d В метод GetResampledData добавлен Cancelation token 2024-11-22 16:48:55 +05:00
1fdd199954 1. Убран Id у DataSaub, теперь в качестве первичного ключа выступает Date
2. Автотесты для методов GetDatesRande, GetResampledData.
3. Рефакторинг
2024-11-22 15:47:00 +05:00
c751f74b35 Убран суффикс "Async" 2024-11-21 17:02:36 +05:00
6518aeabf1 Merge branch 'master' into PersistenceClient 2024-11-21 14:53:03 +05:00
d9cbc9022d Добавить Persistence.Client 2024-11-21 14:50:36 +05:00
098c180b12 Авторизация в интеграционных тестах 2024-11-21 11:41:53 +05:00
b7806493a2 Merge branch 'master' into Client 2024-11-20 16:39:52 +05:00
3a1ea55be2 Авторизация внутри интеграционного теста (начало) 2024-11-20 16:08:16 +05:00
7d5370bf43 Merge branch 'master' into Setpoint 2024-11-20 15:31:07 +05:00
9e55d6791c Внести правки по результатам ревью 2024-11-20 15:29:58 +05:00
e5ffd42fb3 Авторизация через Keycloack 2024-11-20 15:22:23 +05:00
4d24eb9445 Репозиторий для кеширования 2024-11-19 17:51:51 +05:00
b5e255b940 Репозиторий по работе с кешем для временных данных (начало) 2024-11-19 11:32:56 +05:00
5f2535b517 Merge branch 'master' into Setpoint 2024-11-18 15:09:10 +05:00
9e375b4831 Правки 2024-11-18 15:05:12 +05:00
531b14938f Приборка в коде, рефакторинг, автотест для метода get DataSaubController 2024-11-18 14:22:09 +05:00
6fd79f6c4a Fix 2024-11-18 11:39:27 +05:00
90c62b9ede Добавить тестирование Setpoint API 2024-11-18 11:32:57 +05:00
ef72d985be Merge branch 'master' into Setpoint 2024-11-18 10:26:05 +05:00
d746f85fe4 Добавить Setpoint API и репозиторий 2024-11-18 09:39:24 +05:00
84e7ec274c Добавлен слой Persistence.Database.Postgres 2024-11-15 16:29:15 +05:00
92 changed files with 2783 additions and 633 deletions

View File

@ -1,68 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ChangeLogController<TDto, TChangeLogDto> : ControllerBase, IChangeLogApi<TDto, TChangeLogDto>
where TDto : class, IChangeLogAbstract, new()
where TChangeLogDto : ChangeLogDto<TDto>
{
private IChangeLogRepository<TDto, TChangeLogDto> changeLogRepository;
public ChangeLogController(IChangeLogRepository<TDto, TChangeLogDto> changeLogRepository)
{
this.changeLogRepository = changeLogRepository;
}
[HttpGet]
public Task<ActionResult<IEnumerable<TDto>>> GetChangeLogCurrent(CancellationToken token)
{
throw new NotImplementedException();
}
[HttpGet("forDate")]
public Task<ActionResult<IEnumerable<TChangeLogDto>>> GetChangeLogForDate(DateTimeOffset historyMoment, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost]
public Task<ActionResult<int>> AddAsync(TDto dto, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost]
public Task<ActionResult<int>> AddRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost]
public Task<ActionResult<int>> DeleteAsync(int id, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost]
public Task<ActionResult<int>> DeleteRangeAsync(IEnumerable<int> ids, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost("update")]
public Task<ActionResult<int>> UpdateAsync(TDto dto, CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost("update")]
public Task<ActionResult<int>> UpdateRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.API;
using Persistence.Repositories; using Persistence.Repositories;
using Persistence.Repository.Data; using Persistence.Repository.Data;
namespace Persistence.API.Controllers; namespace Persistence.API.Controllers;
[ApiController] [ApiController]
[Authorize]
[Route("api/[controller]")] [Route("api/[controller]")]
public class DataSaubController : TimeSeriesController<DataSaubDto> public class DataSaubController : TimeSeriesController<DataSaubDto>
{ {

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

View File

@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using Persistence.Repositories; using Persistence.Repositories;
namespace Persistence.API.Controllers; namespace Persistence.API.Controllers;
[ApiController] [ApiController]
[Authorize]
[Route("api/[controller]")] [Route("api/[controller]")]
public class TimeSeriesController<TDto> : ControllerBase, ITimeSeriesDataApi<TDto> public class TimeSeriesController<TDto> : ControllerBase, ITimeSeriesDataApi<TDto>
where TDto : class, ITimeSeriesAbstractDto, new() where TDto : class, ITimeSeriesAbstractDto, new()
@ -17,22 +19,32 @@ public class TimeSeriesController<TDto> : ControllerBase, ITimeSeriesDataApi<TDt
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) public async Task<IActionResult> Get(DateTimeOffset dateBegin, CancellationToken token)
{ {
var result = await this.timeSeriesDataRepository.GetAsync(dateBegin, dateEnd, token); var result = await this.timeSeriesDataRepository.GetGtDate(dateBegin, token);
return Ok(result); return Ok(result);
} }
[HttpGet("datesRange")] [HttpGet("datesRange")]
public Task<IActionResult> GetDatesRangeAsync(CancellationToken token) public async Task<IActionResult> GetDatesRange(CancellationToken token)
{ {
throw new NotImplementedException(); var result = await this.timeSeriesDataRepository.GetDatesRange(token);
return Ok(result);
}
[HttpGet("resampled")]
public async Task<IActionResult> GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024, CancellationToken token = default)
{
var result = await this.timeSeriesDataRepository.GetResampledData(dateBegin, intervalSec, approxPointsCount, token);
return Ok(result);
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> InsertRangeAsync(IEnumerable<TDto> dtos, CancellationToken token) public async Task<IActionResult> InsertRange(IEnumerable<TDto> dtos, CancellationToken token)
{ {
var result = await this.timeSeriesDataRepository.InsertRange(dtos, token); var result = await this.timeSeriesDataRepository.InsertRange(dtos, token);
return Ok(result); return Ok(result);
} }
} }

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

View File

@ -0,0 +1,195 @@
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Persistence.Models.Configurations;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Persistence.API;
public static class DependencyInjection
{
public static void AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(c =>
{
c.MapType<TimeSpan>(() => new OpenApiSchema { Type = "string", Example = new OpenApiString("0.00:00:00") });
c.MapType<DateOnly>(() => new OpenApiSchema { Type = "string", Format = "date" });
c.MapType<JsonValue>(() => new OpenApiSchema
{
AnyOf = new OpenApiSchema[]
{
new OpenApiSchema {Type = "string", Format = "string" },
new OpenApiSchema {Type = "number", Format = "int32" },
new OpenApiSchema {Type = "number", Format = "float" },
}
});
c.CustomOperationIds(e =>
{
return $"{e.ActionDescriptor.RouteValues["action"]}";
});
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" });
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>();
if (needUseKeyCloak)
c.AddKeycloackSecurity(configuration);
else c.AddDefaultSecurity(configuration);
//var 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'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]),
}
}
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Keycloack"
},
Scheme = "Bearer",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
#endregion
}

View File

@ -0,0 +1,26 @@
using System.ComponentModel;
using System.Security.Claims;
namespace Persistence.API;
public static class Extensions
{
public static T GetUserId<T>(this ClaimsPrincipal principal)
{
if (principal == null)
throw new ArgumentNullException(nameof(principal));
var loggedInUserId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (String.IsNullOrEmpty(loggedInUserId))
throw new ArgumentNullException(nameof(loggedInUserId));
var result = TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(loggedInUserId);
if (result is null)
throw new ArgumentNullException(nameof(result));
return (T)result;
}
}

View File

@ -8,11 +8,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Persistence.Database.Postgres\Persistence.Database.Postgres.csproj" />
<ProjectReference Include="..\Persistence.Repository\Persistence.Repository.csproj" /> <ProjectReference Include="..\Persistence.Repository\Persistence.Repository.csproj" />
<ProjectReference Include="..\Persistence\Persistence.csproj" /> <ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,10 +1,5 @@
using Microsoft.AspNetCore.Hosting; using Persistence.Models;
using Microsoft.Extensions.Configuration;
using Persistence.Repositories;
using Persistence.Repository;
using Persistence.Repository.Data;
using Persistence.Repository.Repositories;
namespace Persistence.API; namespace Persistence.API;
@ -12,8 +7,9 @@ public class Program
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
var host = CreateHostBuilder(args).Build(); var host = CreateHostBuilder(args).Build();
Persistence.Repository.Startup.BeforeRunHandler(host); Startup.BeforeRunHandler(host);
host.Run(); host.Run();
} }

View File

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

View File

@ -1,4 +1,6 @@
using Persistence.Repository; using Persistence.Repository;
using Persistence.Database.Model;
using Persistence.Database.Postgres;
namespace Persistence.API; namespace Persistence.API;
@ -17,9 +19,10 @@ public class Startup
services.AddControllers(); services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer(); services.AddEndpointsApiExplorer();
services.AddSwaggerGen(); services.AddSwagger(Configuration);
services.AddInfrastructure();
services.AddInfrastructure(Configuration); services.AddPersistenceDbContext(Configuration);
services.AddJWTAuthentication(Configuration);
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@ -30,11 +33,28 @@ public class Startup
app.UseRouting(); app.UseRouting();
//app.UseAuthorization(); if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();
}); });
} }
public static void BeforeRunHandler(IHost host)
{
using var scope = host.Services.CreateScope();
var provider = scope.ServiceProvider;
var context = provider.GetRequiredService<PersistenceDbContext>();
context.Database.EnsureCreatedAndMigrated();
}
} }

View File

@ -1,12 +0,0 @@
namespace Persistence.API;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

@ -4,5 +4,6 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
} },
"NeedUseKeyCloak": false
} }

View File

@ -1,8 +1,16 @@
{ {
"DbConnection": { "DbConnection": {
"Host": "localhost", "Host": "localhost",
"Port": 5432, "Port": 5432,
"Username": "postgres", "Username": "postgres",
"Password": "q" "Password": "q"
} },
"NeedUseKeyCloak": false,
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password"
},
"KeycloakGetTokenUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/token"
} }

View File

@ -6,7 +6,13 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True", "DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True"
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"Authentication": {
"MetadataAddress": "http://192.168.0.10:8321/realms/Persistence/.well-known/openid-configuration",
"Audience": "account",
"ValidIssuer": "http://192.168.0.10:8321/realms/Persistence",
"AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth"
}
} }

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

View File

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Refit;
namespace Persistence.Client.Clients;
public interface ITimeSeriesClient<TDto>
where TDto : class, new()
{
private const string BaseRoute = "/api/dataSaub";
[Post($"{BaseRoute}")]
Task<IApiResponse<int>> InsertRange(IEnumerable<TDto> dtos);
[Get($"{BaseRoute}")]
Task<IApiResponse<IEnumerable<TDto>>> Get(DateTimeOffset dateBegin, DateTimeOffset dateEnd);
[Get($"{BaseRoute}/resampled")]
Task<IApiResponse<IEnumerable<TDto>>> GetResampledData(DateTimeOffset dateBegin, double intervalSec = 600d, int approxPointsCount = 1024);
[Get($"{BaseRoute}/datesRange")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange();
}

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

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

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

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

View File

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Persistence.Database.Model;
public static class DependencyInjection
{
public static IServiceCollection AddPersistenceDbContext(this IServiceCollection services, IConfiguration configuration)
{
string connectionStringName = "DefaultConnection";
services.AddDbContext<PersistenceDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
services.AddScoped<DbContext>(provider => provider.GetRequiredService<PersistenceDbContext>());
return services;
}
}

View File

@ -7,7 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Persistence.Database; namespace Persistence.Database.Postgres;
public static class EFExtensionsInitialization public static class EFExtensionsInitialization
{ {
public static void EnsureCreatedAndMigrated(this DatabaseFacade db) public static void EnsureCreatedAndMigrated(this DatabaseFacade db)

View 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
}
}
}

View File

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

View File

@ -0,0 +1,115 @@
// <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("20241122074646_InitialCreate")]
partial class InitialCreate
{
/// <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<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");
b.Property<double?>("AxialLoad")
.HasColumnType("double precision")
.HasColumnName("axialLoad");
b.Property<double?>("BitDepth")
.HasColumnType("double precision")
.HasColumnName("bitDepth");
b.Property<double?>("BlockPosition")
.HasColumnType("double precision")
.HasColumnName("blockPosition");
b.Property<double?>("BlockSpeed")
.HasColumnType("double precision")
.HasColumnName("blockSpeed");
b.Property<double?>("Flow")
.HasColumnType("double precision")
.HasColumnName("flow");
b.Property<double?>("HookWeight")
.HasColumnType("double precision")
.HasColumnName("hookWeight");
b.Property<int>("IdFeedRegulator")
.HasColumnType("integer")
.HasColumnName("idFeedRegulator");
b.Property<int?>("Mode")
.HasColumnType("integer")
.HasColumnName("mode");
b.Property<double?>("Mse")
.HasColumnType("double precision")
.HasColumnName("mse");
b.Property<short>("MseState")
.HasColumnType("smallint")
.HasColumnName("mseState");
b.Property<double?>("Pressure")
.HasColumnType("double precision")
.HasColumnName("pressure");
b.Property<double?>("Pump0Flow")
.HasColumnType("double precision")
.HasColumnName("pump0Flow");
b.Property<double?>("Pump1Flow")
.HasColumnType("double precision")
.HasColumnName("pump1Flow");
b.Property<double?>("Pump2Flow")
.HasColumnType("double precision")
.HasColumnName("pump2Flow");
b.Property<double?>("RotorSpeed")
.HasColumnType("double precision")
.HasColumnName("rotorSpeed");
b.Property<double?>("RotorTorque")
.HasColumnType("double precision")
.HasColumnName("rotorTorque");
b.Property<string>("User")
.HasColumnType("text")
.HasColumnName("user");
b.Property<double?>("WellDepth")
.HasColumnType("double precision")
.HasColumnName("wellDepth");
b.HasKey("Date");
b.ToTable("DataSaub");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:adminpack", ",,");
migrationBuilder.CreateTable(
name: "DataSaub",
columns: table => new
{
date = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
mode = table.Column<int>(type: "integer", nullable: true),
user = table.Column<string>(type: "text", nullable: true),
wellDepth = table.Column<double>(type: "double precision", nullable: true),
bitDepth = table.Column<double>(type: "double precision", nullable: true),
blockPosition = table.Column<double>(type: "double precision", nullable: true),
blockSpeed = table.Column<double>(type: "double precision", nullable: true),
pressure = table.Column<double>(type: "double precision", nullable: true),
axialLoad = table.Column<double>(type: "double precision", nullable: true),
hookWeight = table.Column<double>(type: "double precision", nullable: true),
rotorTorque = table.Column<double>(type: "double precision", nullable: true),
rotorSpeed = table.Column<double>(type: "double precision", nullable: true),
flow = table.Column<double>(type: "double precision", nullable: true),
mseState = table.Column<short>(type: "smallint", nullable: false),
idFeedRegulator = table.Column<int>(type: "integer", nullable: false),
mse = table.Column<double>(type: "double precision", nullable: true),
pump0Flow = table.Column<double>(type: "double precision", nullable: true),
pump1Flow = table.Column<double>(type: "double precision", nullable: true),
pump2Flow = table.Column<double>(type: "double precision", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DataSaub", x => x.date);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DataSaub");
}
}
}

View File

@ -0,0 +1,136 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Persistence.Database.Model;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistenceDbContext))]
partial class PersistenceDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");
b.Property<double?>("AxialLoad")
.HasColumnType("double precision")
.HasColumnName("axialLoad");
b.Property<double?>("BitDepth")
.HasColumnType("double precision")
.HasColumnName("bitDepth");
b.Property<double?>("BlockPosition")
.HasColumnType("double precision")
.HasColumnName("blockPosition");
b.Property<double?>("BlockSpeed")
.HasColumnType("double precision")
.HasColumnName("blockSpeed");
b.Property<double?>("Flow")
.HasColumnType("double precision")
.HasColumnName("flow");
b.Property<double?>("HookWeight")
.HasColumnType("double precision")
.HasColumnName("hookWeight");
b.Property<int>("IdFeedRegulator")
.HasColumnType("integer")
.HasColumnName("idFeedRegulator");
b.Property<int?>("Mode")
.HasColumnType("integer")
.HasColumnName("mode");
b.Property<double?>("Mse")
.HasColumnType("double precision")
.HasColumnName("mse");
b.Property<short>("MseState")
.HasColumnType("smallint")
.HasColumnName("mseState");
b.Property<double?>("Pressure")
.HasColumnType("double precision")
.HasColumnName("pressure");
b.Property<double?>("Pump0Flow")
.HasColumnType("double precision")
.HasColumnName("pump0Flow");
b.Property<double?>("Pump1Flow")
.HasColumnType("double precision")
.HasColumnName("pump1Flow");
b.Property<double?>("Pump2Flow")
.HasColumnType("double precision")
.HasColumnName("pump2Flow");
b.Property<double?>("RotorSpeed")
.HasColumnType("double precision")
.HasColumnName("rotorSpeed");
b.Property<double?>("RotorTorque")
.HasColumnType("double precision")
.HasColumnName("rotorTorque");
b.Property<string>("User")
.HasColumnType("text")
.HasColumnName("user");
b.Property<double?>("WellDepth")
.HasColumnType("double precision")
.HasColumnName("wellDepth");
b.HasKey("Date");
b.ToTable("DataSaub");
});
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
}
}
}

View 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.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence.Database\Persistence.Database.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -1,22 +1,27 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System; using Npgsql;
using System.Collections.Generic; using Persistence.Database.Entity;
using System.Diagnostics.Metrics; using System.Data.Common;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
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 PersistenceDbContext() { } // Áåç ïóñòîãî êîíñòðóêòîðà ìèãðàöèÿ ïàäààåò public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options) public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
public PersistenceDbContext()
: base()
{ {
}
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options)
: base(options)
{
} }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
@ -31,7 +36,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();
} }
} }

View File

@ -0,0 +1,5 @@
## Создать миграцию
```
dotnet ef migrations add <MigrationName> --project Persistence.Database.Postgres
```

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

View File

@ -1,13 +1,12 @@
using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Persistence.Database.Model; namespace Persistence.Database.Model;
public class DataSaub : ITimestampedData public class DataSaub : ITimestampedData
{ {
[Column("id")] [Key, Column("date")]
public int Id { get; set; } public DateTimeOffset Date { get; set; }
[Column("timestamp")]
public int TimeStamp { get; set; }
[Column("mode")] [Column("mode")]
public int? Mode { get; set; } public int? Mode { get; set; }

View File

@ -7,5 +7,8 @@ using System.Threading.Tasks;
namespace Persistence.Database.Model; namespace Persistence.Database.Model;
public interface ITimestampedData public interface ITimestampedData
{ {
int TimeStamp { get; set; } /// <summary>
/// Дата (должна быть обязательно в UTC)
/// </summary>
DateTimeOffset Date { get; set; }
} }

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

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

View File

@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Persistence.Database.Model
{
public class ChangeLog : IChangeLogData
{
[Column("id")]
public int Id { get; set; }
[Column("idnext")]
public int? IdNext { get; set; }
[Column("idprevious")]
public int? IdPrevious { get; set; }
[Column("creation")]
public DateTimeOffset Creation { get; set; }
[Column("obsolete")]
public DateTimeOffset? Obsolete { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Persistence.Database.Model
{
internal interface IChangeLogData
{
public int Id { get; set; }
public int? IdNext { get; set; }
public int? IdPrevious { get; set; }
public DateTimeOffset Creation { get; set; }
public DateTimeOffset? Obsolete { get; set; }
}
}

View File

@ -1,21 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
public interface IDbContextManager
{
//IConnectionManager ConnectionManager { get; }
DbContext GetReadonlyDbContext();
DbContext GetDbContext();
DbContext CreateAndInitializeNewContext();
DbContext CreateAndInitializeNewContext(DbConnection connection);
}

View File

@ -1,17 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
public interface IPersistenceDbContext : IDisposable
{
DbSet<DataSaub> DataSaub { get; }
DbSet<ChangeLog> ChangeLog { get; }
DatabaseFacade Database { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

View File

@ -7,11 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@ -17,9 +17,9 @@ public abstract class BaseIntegrationTest : IClassFixture<WebAppFactoryFixture>,
protected BaseIntegrationTest(WebAppFactoryFixture factory) protected BaseIntegrationTest(WebAppFactoryFixture factory)
{ {
//scope = factory.Services.CreateScope(); scope = factory.Services.CreateScope();
//dbContext = scope.ServiceProvider.GetRequiredService<PersistenceDbContext>(); dbContext = scope.ServiceProvider.GetRequiredService<PersistenceDbContext>();
} }
public void Dispose() public void Dispose()

View File

@ -1,16 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Repository.Data;
using Refit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.IntegrationTests.Clients;
public interface ITimeSeriesClient<TDto>
where TDto : class, new()
{
[Post("/api/dataSaub")]
Task<IApiResponse<int>> InsertRangeAsync(IEnumerable<TDto> dtos);
}

View File

@ -1,36 +1,55 @@
using Persistence.Repository.Data; using Persistence.Client;
using System; using Persistence.Database.Model;
using System.Collections.Generic; using Persistence.Repository.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit; using Xunit;
namespace Persistence.IntegrationTests.Controllers; namespace Persistence.IntegrationTests.Controllers;
public class DataSaubControllerTest : TimeSeriesBaseControllerTest<DataSaubDto> public class DataSaubControllerTest : TimeSeriesBaseControllerTest<DataSaub, DataSaubDto>
{ {
private readonly DataSaubDto dto = new DataSaubDto() private readonly DataSaubDto dto = new DataSaubDto()
{ {
AxialLoad = 1, AxialLoad = 1,
BitDepth = 2, BitDepth = 2,
BlockPosition = 3, BlockPosition = 3,
BlockSpeed = 4, BlockSpeed = 4,
Date = DateTimeOffset.Now, Date = DateTimeOffset.UtcNow,
Flow = 5, Flow = 5,
HookWeight = 6, HookWeight = 6,
Id = 7, IdFeedRegulator = 8,
IdFeedRegulator = 8, Mode = 9,
Mode = 9, Mse = 10,
Mse = 10, MseState = 11,
MseState = 11, Pressure = 12,
Pressure = 12, Pump0Flow = 13,
Pump0Flow = 13, Pump1Flow = 14,
Pump1Flow = 14, Pump2Flow = 15,
Pump2Flow = 15, RotorSpeed = 16,
RotorSpeed = 16, RotorTorque = 17,
RotorTorque = 17, User = string.Empty,
User = string.Empty, WellDepth = 18,
WellDepth = 18, };
private readonly DataSaub entity = new DataSaub()
{
AxialLoad = 1,
BitDepth = 2,
BlockPosition = 3,
BlockSpeed = 4,
Date = DateTimeOffset.UtcNow,
Flow = 5,
HookWeight = 6,
IdFeedRegulator = 8,
Mode = 9,
Mse = 10,
MseState = 11,
Pressure = 12,
Pump0Flow = 13,
Pump1Flow = 14,
Pump2Flow = 15,
RotorSpeed = 16,
RotorTorque = 17,
User = string.Empty,
WellDepth = 18,
}; };
public DataSaubControllerTest(WebAppFactoryFixture factory) : base(factory) public DataSaubControllerTest(WebAppFactoryFixture factory) : base(factory)
@ -42,4 +61,26 @@ public class DataSaubControllerTest : TimeSeriesBaseControllerTest<DataSaubDto>
{ {
await InsertRangeSuccess(dto); await InsertRangeSuccess(dto);
} }
[Fact]
public async Task Get_returns_success()
{
var beginDate = DateTimeOffset.UtcNow.AddDays(-1);
var endDate = DateTimeOffset.UtcNow;
await GetSuccess(beginDate, endDate, entity);
}
[Fact]
public async Task GetDatesRange_returns_success()
{
await GetDatesRangeSuccess(entity);
}
[Fact]
public async Task GetResampledData_returns_success()
{
await GetResampledDataSuccess(entity);
}
} }

View File

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

View File

@ -1,26 +1,27 @@
using Mapster;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Persistence.IntegrationTests.Clients;
using Persistence.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Text; using Mapster;
using System.Threading.Tasks; 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;
public abstract class TimeSeriesBaseControllerTest<TDto> : BaseIntegrationTest public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrationTest
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>();
client = factory.GetHttpClient<ITimeSeriesClient<TDto>>(string.Empty);
var scope = factory.Services.CreateScope();
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
timeSeriesClient = persistenceClientFactory.GetClient<ITimeSeriesClient<TDto>>();
} }
public async Task InsertRangeSuccess(TDto dto) public async Task InsertRangeSuccess(TDto dto)
@ -29,23 +30,96 @@ public abstract class TimeSeriesBaseControllerTest<TDto> : BaseIntegrationTest
var expected = dto.Adapt<TDto>(); var expected = dto.Adapt<TDto>();
//act //act
var response = await client.InsertRangeAsync(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);
Assert.Equal(1, response.Content); Assert.Equal(1, response.Content);
//var entity = GetByWellId();
//Assert.NotNull(entity);
//var actual = entity.Adapt<ChangeLogDto<TDto>>();
//Assert.Equal(ProcessMapPlanBase.IdStateActual, actual.IdState);
//var excludeProps = new[] {
// nameof(ProcessMapPlanBaseDto.Id),
// nameof(ProcessMapPlanBaseDto.Section)
//};
//MatchHelper.Match(expected, actual.Item, excludeProps);
} }
public async Task GetSuccess(DateTimeOffset beginDate, DateTimeOffset endDate, TEntity entity)
{
//arrange
var dbset = dbContext.Set<TEntity>();
dbset.Add(entity);
dbContext.SaveChanges();
var response = await timeSeriesClient.Get(beginDate, endDate);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Single(response.Content);
}
public async Task GetDatesRangeSuccess(TEntity entity)
{
//arrange
var datesRangeExpected = 30;
var entity2 = entity.Adapt<TEntity>();
entity2.Date = entity.Date.AddDays(datesRangeExpected);
var dbset = dbContext.Set<TEntity>();
dbset.Add(entity);
dbset.Add(entity2);
dbContext.SaveChanges();
var response = await timeSeriesClient.GetDatesRange();
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var datesRangeActual = (response.Content.To - response.Content.From).Days;
Assert.Equal(datesRangeExpected, datesRangeActual);
}
public async Task GetResampledDataSuccess(TEntity entity)
{
//arrange
var approxPointsCount = 10;
var differenceBetweenStartAndEndDays = 50;
var entities = new List<TEntity>();
for (var i = 1; i <= differenceBetweenStartAndEndDays; i++)
{
var entity2 = entity.Adapt<TEntity>();
entity2.Date = entity.Date.AddDays(i - 1);
entities.Add(entity2);
}
var dbset = dbContext.Set<TEntity>();
dbset.AddRange(entities);
dbContext.SaveChanges();
var response = await timeSeriesClient.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
var ratio = entities.Count() / approxPointsCount;
if (ratio > 1)
{
var expectedResampledCount = entities
.Where((_, index) => index % ratio == 0)
.Count();
Assert.Equal(expectedResampledCount, response.Content.Count());
}
else
{
Assert.Equal(entities.Count(), response.Content.Count());
}
}
} }

View File

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

View File

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
namespace Persistence.IntegrationTests;
public static class EFCoreExtensions
{
public static void CleanupDbSet<T>(this DbContext dbContext)
where T : class
{
var dbset = dbContext.Set<T>();
dbset.RemoveRange(dbset);
dbContext.SaveChanges();
}
}

View File

@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Refit" Version="8.0.0" /> <PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -24,6 +25,8 @@
<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" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

View File

@ -1,60 +1,56 @@
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 Persistence.Database.Model; using Microsoft.Extensions.DependencyInjection.Extensions;
using Persistence.API; using Persistence.API;
using Refit; using Persistence.Database;
using System.Net.Http.Headers; using Persistence.Client;
using System.Text.Json; using Persistence.Database.Model;
using Persistence.Database.Postgres;
using RestSharp;
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;
public WebAppFactoryFixture()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Tests.json")
.Build();
var dbConnection = configuration.GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
}
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.ConfigureServices(services => builder.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("appsettings.Tests.json");
var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
});
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));
var serviceProvider = services.BuildServiceProvider(); services.RemoveAll<IHttpClientFactory>();
services.AddSingleton<IHttpClientFactory>(provider =>
{
return new TestHttpClientFactory(this);
});
services.AddSingleton<PersistenceClientFactory>();
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>();
//dbContext.Database.EnsureCreatedAndMigrated(); var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
//dbContext.Deposits.AddRange(Data.Defaults.Deposits); dbContext.Database.EnsureCreatedAndMigrated();
dbContext.SaveChanges(); dbContext.SaveChanges();
}); });
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -66,36 +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 T GetAuthorizedHttpClient<T>(string uriSuffix)
//{
// var httpClient = 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 HttpClient GetAuthorizedHttpClient()
//{
// var httpClient = CreateClient();
// var jwtToken = ApiTokenHelper.GetAdminUserToken();
// httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
// return httpClient;
//}
} }

View File

@ -0,0 +1,199 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Repository;
/// <summary>
/// Цикличный массив
/// </summary>
/// <typeparam name="T"></typeparam>
public class CyclicArray<T> : IEnumerable<T>
{
readonly T[] array;
int used, current = -1;
/// <summary>
/// constructor
/// </summary>
/// <param name="capacity"></param>
public CyclicArray(int capacity)
{
array = new T[capacity];
}
/// <summary>
/// Количество элементов в массиве
/// </summary>
public int Count => used;
/// <summary>
/// Добавить новый элемент<br/>
/// Если capacity достигнуто, то вытеснит самый первый элемент
/// </summary>
/// <param name="item"></param>
public void Add(T item)
{
current = (++current) % array.Length;
array[current] = item;
if (used < array.Length)
used++;
UpdatedInvoke(current, item);
}
/// <summary>
/// Добавить новые элементы.<br/>
/// Если capacity достигнуто, то вытеснит самые первые элементы.<br/>
/// Не вызывает Updated!
/// </summary>
/// <param name="items"></param>
public void AddRange(IEnumerable<T> items)
{
var capacity = array.Length;
var newItems = items.TakeLast(capacity).ToArray();
if (newItems.Length == capacity)
{
Array.Copy(newItems, array, capacity);
current = capacity - 1;
}
else
{
current = (++current) % capacity;
var countToEndOfArray = capacity - current;
if (newItems.Length <= countToEndOfArray)
{
Array.Copy(newItems, 0, array, current, newItems.Length);
current += newItems.Length - 1;
}
else
{
var firstStepLength = countToEndOfArray;
Array.Copy(newItems, 0, array, current, firstStepLength);
var secondStepCount = newItems.Length - firstStepLength;
Array.Copy(newItems, firstStepLength, array, 0, secondStepCount);
current = secondStepCount - 1;
}
}
if (used < capacity)
{
used += newItems.Length;
used = used > capacity ? capacity : used;
}
}
/// <summary>
/// Индекс
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public T this[int index]
{
get
{
if (used == 0)
throw new IndexOutOfRangeException();
var i = (current + 1 + index) % used;
return array[i];
}
set
{
var devider = used > 0 ? used : array.Length;
var i = (current + 1 + index) % devider;
array[i] = value;
UpdatedInvoke(current, value);
}
}
/// <summary>
/// событие на изменение элемента в массиве
/// </summary>
public event EventHandler<(int index, T value)>? Updated;
private void UpdatedInvoke(int index, T value)
{
Updated?.Invoke(this, (index, value));
}
/// <summary>
/// Агрегирование значения по всему массиву
/// </summary>
/// <typeparam name="Tout"></typeparam>
/// <param name="func"></param>
/// <param name="startValue"></param>
/// <returns></returns>
public Tout Aggregate<Tout>(Func<T, Tout, Tout> func, Tout startValue)
{
Tout result = startValue;
for (int i = 0; i < used; i++)
result = func(this[i], result);
return result;
}
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
=> new CyclycListEnumerator<T>(array, current, used);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
class CyclycListEnumerator<Te> : IEnumerator<Te>
{
private readonly Te[] array;
private readonly int used;
private readonly int first;
private int current = -1;
public CyclycListEnumerator(Te[] array, int first, int used)
{
this.array = new Te[array.Length];
array.CopyTo(this.array, 0);
this.used = used;
this.first = first;
}
public Te Current
{
get
{
if (IsCurrentOk())
{
var i = (current + first + 1) % used;
return array[i];
}
else
return default!;
}
}
object? IEnumerator.Current => Current;
public void Dispose() {; }
private bool IsCurrentOk() => current >= 0 && current < used;
public bool MoveNext()
{
if (current < used)
current++;
return IsCurrentOk();
}
public void Reset()
{
current = -1;
}
}
/// <summary>
/// Очистить весь массив
/// </summary>
public void Clear()
{
used = 0;
current = -1;
}
}

View File

@ -4,8 +4,6 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Persistence.Repository.Data; namespace Persistence.Repository.Data;
public class DataSaubDto : ITimeSeriesAbstractDto public class DataSaubDto : ITimeSeriesAbstractDto
{ {
public int Id { get; set; }
public DateTimeOffset Date { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset Date { get; set; } = DateTimeOffset.UtcNow;
public int? Mode { get; set; } public int? Mode { get; set; }

View File

@ -1,16 +0,0 @@
using Persistence.Models;
namespace Persistence.Repository.Data
{
internal class LogDto : IChangeLogAbstract
{
public int Id { get; set; }
public int IdAuthor { get; set; }
public int? IdNext { get; set; }
public int? IdPrevious { get; set; }
public int? IdEditor { get; set; }
public int IdState { get; set; }
public DateTimeOffset Creation { get; set; }
public DateTimeOffset? Obsolete { get; set; }
}
}

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

View File

@ -1,11 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Persistence.Repositories;
using Persistence.Database.Model; using Persistence.Database.Model;
using Persistence.Repositories;
using Persistence.Repository.Data; using Persistence.Repository.Data;
using Persistence.Repository.Repositories; using Persistence.Repository.Repositories;
using Persistence.Models;
namespace Persistence.Repository; namespace Persistence.Repository;
public static class DependencyInjection public static class DependencyInjection
@ -13,20 +10,16 @@ public static class DependencyInjection
public static void MapsterSetup() public static void MapsterSetup()
{ {
} }
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{ {
MapsterSetup(); MapsterSetup();
string connectionStringName = "DefaultConnection";
services.AddDbContext<PersistenceDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
services.AddScoped<IPersistenceDbContext>(provider => provider.GetRequiredService<PersistenceDbContext>());
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>(); services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
services.AddTransient<IChangeLogRepository<LogDto, ChangeLogDto<LogDto>>, ChangeLogRepository<ChangeLog, LogDto, ChangeLogDto<LogDto>>>(); services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
return services; return services;
} }
} }

View File

@ -1,71 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.Repository.Repositories
{
public abstract class ChangeLogRepository<TEntity, TDto, TChangeLogDto> : IChangeLogRepository<TDto, TChangeLogDto>
where TEntity : class
where TDto : class, IChangeLogAbstract, new()
where TChangeLogDto : ChangeLogDto<TDto>
{
private DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<TEntity> GetQueryReadOnly() => db.Set<TEntity>();
public Task<int> Clear(int idUser, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
}
}

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

View File

@ -0,0 +1,105 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using Persistence.Database.Model;
using Persistence.Models;
namespace Persistence.Repository.Repositories;
public class TimeSeriesDataCachedRepository<TEntity, TDto> : TimeSeriesDataRepository<TEntity, TDto>
where TEntity : class, ITimestampedData, new()
where TDto : class, ITimeSeriesAbstractDto, new()
{
public static TDto? FirstByDate { get; private set; }
public static CyclicArray<TDto> LastData { get; } = new CyclicArray<TDto>(CacheItemsCount);
private const int CacheItemsCount = 3600;
public TimeSeriesDataCachedRepository(DbContext db) : base(db)
{
Task.Run(async () =>
{
var firstDateItem = await base.GetFirstAsync(CancellationToken.None);
if (firstDateItem == null)
{
return;
}
FirstByDate = firstDateItem;
var dtos = await base.GetLastAsync(CacheItemsCount, CancellationToken.None);
dtos = dtos.OrderBy(d => d.Date);
LastData.AddRange(dtos);
}).Wait();
}
public override async Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token)
{
if (LastData.Count() == 0 || LastData[0].Date > dateBegin)
{
var dtos = await base.GetGtDate(dateBegin, token);
return dtos;
}
var items = LastData
.Where(i => i.Date >= dateBegin);
return items;
}
public override async Task<int> InsertRange(IEnumerable<TDto> dtos, CancellationToken token)
{
var result = await base.InsertRange(dtos, token);
if (result > 0)
{
dtos = dtos.OrderBy(x => x.Date);
FirstByDate = dtos.First();
LastData.AddRange(dtos);
}
return result;
}
public override async Task<DatesRangeDto?> GetDatesRange(CancellationToken token)
{
if (FirstByDate == null)
return null;
return await Task.Run(() =>
{
return new DatesRangeDto
{
From = FirstByDate.Date,
To = LastData[^1].Date
};
});
}
public override async Task<IEnumerable<TDto>> GetResampledData(
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default)
{
var dtos = LastData.Where(i => i.Date >= dateBegin);
if (LastData.Count == 0 || LastData[0].Date > dateBegin)
{
dtos = await base.GetGtDate(dateBegin, token);
}
var dateEnd = dateBegin.AddSeconds(intervalSec);
dtos = dtos
.Where(i => i.Date <= dateEnd);
var ratio = dtos.Count() / approxPointsCount;
if (ratio > 1)
dtos = dtos
.Where((_, index) => index % ratio == 0);
return dtos;
}
}

View File

@ -1,13 +1,12 @@
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models; using Persistence.Models;
using Persistence.Repositories; using Persistence.Repositories;
using Persistence.Database.Model;
using Persistence.Repository.Data;
namespace Persistence.Repository.Repositories; namespace Persistence.Repository.Repositories;
public abstract class TimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataRepository<TDto> public class TimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataRepository<TDto>
where TEntity : class where TEntity : class, ITimestampedData, new()
where TDto : class, ITimeSeriesAbstractDto, new() where TDto : class, ITimeSeriesAbstractDto, new()
{ {
private DbContext db; private DbContext db;
@ -17,28 +16,32 @@ public abstract class TimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataR
this.db = db; this.db = db;
} }
protected virtual IQueryable<TEntity> GetQueryReadOnly() => db.Set<TEntity>(); protected virtual IQueryable<TEntity> GetQueryReadOnly() => this.db.Set<TEntity>();
public async Task<IEnumerable<TDto>> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) public virtual async Task<DatesRangeDto?> GetDatesRange(CancellationToken token)
{ {
var query = GetQueryReadOnly(); var query = GetQueryReadOnly();
var minDate = await query.MinAsync(o => o.Date, token);
var maxDate = await query.MaxAsync(o => o.Date, token);
return new DatesRangeDto
{
From = minDate,
To = maxDate
};
}
public virtual async Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
{
var query = this.db.Set<TEntity>().Where(e => e.Date > date);
var entities = await query.ToArrayAsync(token); var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<TDto>()); var dtos = entities.Select(e => e.Adapt<TDto>());
return dtos; return dtos;
} }
public Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token) public virtual async Task<int> InsertRange(IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
{
throw new NotImplementedException();
}
public async Task<int> InsertRange(IEnumerable<TDto> dtos, CancellationToken token)
{ {
var entities = dtos.Select(d => d.Adapt<TEntity>()); var entities = dtos.Select(d => d.Adapt<TEntity>());
@ -47,4 +50,50 @@ public abstract class TimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataR
return result; return result;
} }
protected async Task<IEnumerable<TDto>> GetLastAsync(int takeCount, CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderByDescending(e => e.Date)
.Take(takeCount);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<TDto>());
return dtos;
}
protected async Task<TDto?> GetFirstAsync(CancellationToken token)
{
var query = GetQueryReadOnly()
.OrderBy(e => e.Date);
var entity = await query.FirstOrDefaultAsync(token);
if (entity == null)
return null;
var dto = entity.Adapt<TDto>();
return dto;
}
public async virtual Task<IEnumerable<TDto>> GetResampledData(
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default)
{
var dtos = await GetGtDate(dateBegin, token);
var dateEnd = dateBegin.AddSeconds(intervalSec);
dtos = dtos
.Where(i => i.Date <= dateEnd);
var ratio = dtos.Count() / approxPointsCount;
if (ratio > 1)
dtos = dtos
.Where((_, index) => index % ratio == 0);
return dtos;
}
} }

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

View File

@ -1,19 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Persistence.Database;
using Persistence.Database.Model;
namespace Persistence.Repository;
public class Startup
{
public static void BeforeRunHandler(IHost host)
{
using var scope = host.Services.CreateScope();
var provider = scope.ServiceProvider;
var context = provider.GetRequiredService<DbContext>();
context.Database.EnsureCreatedAndMigrated();
}
}

View File

@ -11,7 +11,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Repository", "P
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Persistence.Database\Persistence.Database.csproj", "{F77475D1-D074-407A-9D69-2FADDDAE2056}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Persistence.Database\Persistence.Database.csproj", "{F77475D1-D074-407A-9D69-2FADDDAE2056}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
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
@ -39,6 +43,14 @@ Global
{10752C25-3773-4081-A1F2-215A1D950126}.Debug|Any CPU.Build.0 = Debug|Any CPU {10752C25-3773-4081-A1F2-215A1D950126}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10752C25-3773-4081-A1F2-215A1D950126}.Release|Any CPU.ActiveCfg = Release|Any CPU {10752C25-3773-4081-A1F2-215A1D950126}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10752C25-3773-4081-A1F2-215A1D950126}.Release|Any CPU.Build.0 = Release|Any CPU {10752C25-3773-4081-A1F2-215A1D950126}.Release|Any CPU.Build.0 = Release|Any CPU
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Debug|Any CPU.ActiveCfg = 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.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

View File

@ -31,7 +31,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> AddAsync(TDto dto, CancellationToken token); Task<ActionResult<int>> Add(TDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Добавить несколько записей /// Добавить несколько записей
@ -39,7 +39,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> AddRangeAsync(IEnumerable<TDto> dtos, CancellationToken token); Task<ActionResult<int>> AddRange(IEnumerable<TDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Обновить одну запись /// Обновить одну запись
@ -47,7 +47,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> UpdateAsync(TDto dto, CancellationToken token); Task<ActionResult<int>> Update(TDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Обновить несколько записей /// Обновить несколько записей
@ -55,7 +55,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> UpdateRangeAsync(IEnumerable<TDto> dtos, CancellationToken token); Task<ActionResult<int>> UpdateRange(IEnumerable<TDto> dtos, CancellationToken token);
/// <summary> /// <summary>
/// Удалить одну запись /// Удалить одну запись
@ -63,7 +63,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> DeleteAsync(int id, CancellationToken token); Task<ActionResult<int>> Delete(int id, CancellationToken token);
/// <summary> /// <summary>
/// Удалить несколько записей /// Удалить несколько записей
@ -71,5 +71,5 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="ids"></param> /// <param name="ids"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> DeleteRangeAsync(IEnumerable<int> ids, CancellationToken token); Task<ActionResult<int>> DeleteRange(IEnumerable<int> ids, CancellationToken token);
} }

View File

@ -13,7 +13,7 @@ public interface IDictionaryElementApi<TDto> where TDto : class, new()
/// <param name="dictionaryKey">ключ справочника</param> /// <param name="dictionaryKey">ключ справочника</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetAsync(Guid dictionaryKey, CancellationToken token); Task<ActionResult<IEnumerable<TDto>>> Get(Guid dictionaryKey, CancellationToken token);
/// <summary> /// <summary>
/// Добавить элемент в справочник /// Добавить элемент в справочник
@ -22,7 +22,7 @@ public interface IDictionaryElementApi<TDto> where TDto : class, new()
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<Guid>> AddAsync(Guid dictionaryKey, TDto dto, CancellationToken token); Task<ActionResult<Guid>> Add(Guid dictionaryKey, TDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Изменить одну запись /// Изменить одну запись
@ -32,7 +32,7 @@ public interface IDictionaryElementApi<TDto> where TDto : class, new()
/// <param name="dto"></param> /// <param name="dto"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<Guid>> UpdateAsync(Guid dictionaryKey, Guid dictionaryElementKey, TDto dto, CancellationToken token); Task<ActionResult<Guid>> Update(Guid dictionaryKey, Guid dictionaryElementKey, TDto dto, CancellationToken token);
/// <summary> /// <summary>
/// Удалить одну запись /// Удалить одну запись
@ -41,5 +41,5 @@ public interface IDictionaryElementApi<TDto> where TDto : class, new()
/// <param name="dictionaryElementKey">ключ элемента в справочнике</param> /// <param name="dictionaryElementKey">ключ элемента в справочнике</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> DeleteAsync(Guid dictionaryKey, Guid dictionaryElementKey, CancellationToken token); Task<ActionResult<int>> Delete(Guid dictionaryKey, Guid dictionaryElementKey, CancellationToken token);
} }

View File

@ -14,7 +14,7 @@ public interface ISetpointApi
/// <param name="setpoitKeys">ключи уставок</param> /// <param name="setpoitKeys">ключи уставок</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrentAsync(IEnumerable<Guid> setpoitKeys, CancellationToken token); Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent(IEnumerable<Guid> setpoitKeys, CancellationToken token);
/// <summary> /// <summary>
/// Получить значения уставок за определенный момент времени /// Получить значения уставок за определенный момент времени
@ -23,7 +23,7 @@ public interface ISetpointApi
/// <param name="historyMoment">дата, на которую получаем данные</param> /// <param name="historyMoment">дата, на которую получаем данные</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<SetpointValueDto>>> GetHistoryAsync(IEnumerable<Guid> setpoitKeys, DateTimeOffset historyMoment, CancellationToken token); Task<ActionResult<IEnumerable<SetpointValueDto>>> GetHistory(IEnumerable<Guid> setpoitKeys, DateTimeOffset historyMoment, CancellationToken token);
/// <summary> /// <summary>
/// Получить историю изменений значений уставок /// Получить историю изменений значений уставок
@ -31,7 +31,7 @@ public interface ISetpointApi
/// <param name="setpoitKeys">ключи уставок</param> /// <param name="setpoitKeys">ключи уставок</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLogAsync(IEnumerable<Guid> setpoitKeys, CancellationToken token); Task<ActionResult<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog(IEnumerable<Guid> setpoitKeys, CancellationToken token);
/// <summary> /// <summary>
/// Метод сохранения уставки /// Метод сохранения уставки
@ -40,5 +40,5 @@ public interface ISetpointApi
/// <param name="newValue">значение</param> /// <param name="newValue">значение</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<int>> SaveAsync(Guid setpointKey, object newValue, CancellationToken token); Task<ActionResult<int>> Save(Guid setpointKey, object newValue, CancellationToken token);
} }

View File

@ -15,7 +15,7 @@ public interface ISyncApi<TDto> where TDto : class, new()
/// <param name="take">количество записей</param> /// <param name="take">количество записей</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetPartAsync(DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default); Task<ActionResult<IEnumerable<TDto>>> GetPart(DateTimeOffset dateBegin, int take = 24 * 60 * 60, CancellationToken token = default);
/// <summary> /// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории /// Получить диапазон дат, для которых есть данные в репозитории

View File

@ -19,5 +19,5 @@ public interface ITableDataApi<TDto, TRequest>
/// <param name="request">параметры фильтрации</param> /// <param name="request">параметры фильтрации</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<PaginationContainer<TDto>>> GetPageAsync(TRequest request, CancellationToken token); Task<ActionResult<PaginationContainer<TDto>>> GetPage(TRequest request, CancellationToken token);
} }

View File

@ -4,23 +4,27 @@ using Persistence.Models;
namespace Persistence.API; namespace Persistence.API;
/// <summary> /// <summary>
/// Интерфейс для работы с API графиков /// Базовый интерфейс для работы с временными рядами
/// </summary> /// </summary>
public interface IGraphDataApi<TDto> public interface ITimeSeriesBaseDataApi<TDto>
{ {
/// <summary> /// <summary>
/// Получить список объектов с прореживанием, удовлетворящий диапазону дат /// Получить список объектов с прореживанием, удовлетворяющий диапазону дат
/// </summary> /// </summary>
/// <param name="dateBegin">дата начала</param> /// <param name="dateBegin">дата начала</param>
/// <param name="dateEnd">дата окончания</param> /// <param name="dateEnd">дата окончания</param>
/// <param name="approxPointsCount"></param> /// <param name="approxPointsCount"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetThinnedDataAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, int approxPointsCount = 1024); Task<IActionResult> GetResampledData(
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default);
/// <summary> /// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории /// Получить диапазон дат, для которых есть данные в репозитории
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<ActionResult<DatesRangeDto>> GetDatesRangeAsync(CancellationToken token); Task<IActionResult> GetDatesRange(CancellationToken token);
} }

View File

@ -11,24 +11,16 @@ namespace Persistence.API;
/// <summary> /// <summary>
/// Интерфейс для работы с API временных данных /// Интерфейс для работы с API временных данных
/// </summary> /// </summary>
public interface ITimeSeriesDataApi<TDto> public interface ITimeSeriesDataApi<TDto> : ITimeSeriesBaseDataApi<TDto>
where TDto : class, ITimeSeriesAbstractDto, new() where TDto : class, ITimeSeriesAbstractDto, new()
{ {
/// <summary> /// <summary>
/// Получить список объектов, удовлетворяющий диапазон дат /// Получить список объектов, удовлетворяющий диапазон дат
/// </summary> /// </summary>
/// <param name="dateBegin">дата начала</param> /// <param name="dateBegin">дата начала</param>
/// <param name="dateEnd">дата окончания</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token); Task<IActionResult> Get(DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetDatesRangeAsync(CancellationToken token);
/// <summary> /// <summary>
/// Добавление записей /// Добавление записей
@ -36,7 +28,7 @@ public interface ITimeSeriesDataApi<TDto>
/// <param name="dtos"></param> /// <param name="dtos"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IActionResult> InsertRangeAsync(IEnumerable<TDto> dtos, CancellationToken token); Task<IActionResult> InsertRange(IEnumerable<TDto> dtos, CancellationToken token);
} }

View File

@ -26,7 +26,7 @@ public class ChangeLogDto<T> where T: class
public DateTimeOffset Creation { get; set; } public DateTimeOffset Creation { get; set; }
/// <summary> /// <summary>
/// Дата устаревания (например при удалении) /// Дата устаревания (например, при удалении)
/// </summary> /// </summary>
public DateTimeOffset? Obsolete { get; set; } public DateTimeOffset? Obsolete { get; set; }

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

View 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";
}
}

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

View File

@ -5,9 +5,19 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Persistence.Models; namespace Persistence.Models;
/// <summary>
/// Диапазон дат
/// </summary>
public class DatesRangeDto public class DatesRangeDto
{ {
public DateTimeOffset dateBegin { get; set; } /// <summary>
/// Дата начала диапазона
/// </summary>
public DateTimeOffset From { get; set; }
public DateTimeOffset dateEnd { get; set; } /// <summary>
/// Дата окончания диапазона
/// </summary>
public DateTimeOffset To { get; set; }
} }

View File

@ -59,9 +59,4 @@ public interface IChangeLogAbstract
/// Id заменяемой записи /// Id заменяемой записи
/// </summary> /// </summary>
public int? IdPrevious { get; set; } public int? IdPrevious { get; set; }
/// <summary>
/// Id последующей записи
/// </summary>
public int? IdNext { get; set; }
} }

View File

@ -1,10 +1,4 @@
using System; namespace Persistence.Models;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models;
/// <summary> /// <summary>
/// Интерфейс, описывающий временные данные /// Интерфейс, описывающий временные данные

View File

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

View File

@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models; namespace Persistence.Models;
/// <summary>
/// Модель для описания лога уставки
/// </summary>
public class SetpointLogDto : SetpointValueDto public class SetpointLogDto : SetpointValueDto
{ {
public DateTimeOffset Edit { get; set; } /// <summary>
/// Дата сохранения уставки
/// </summary>
public DateTimeOffset Created { get; set; }
/// <summary>
/// Ключ пользователя
/// </summary>
public int IdUser { get; set; } public int IdUser { get; set; }
} }

View File

@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models; namespace Persistence.Models;
/// <summary>
/// Модель для хранения значения уставки
/// </summary>
public class SetpointValueDto public class SetpointValueDto
{ {
public int Id { get; set; } /// Идентификатор уставки
public object Value { get; set; } /// <summary>
/// </summary>
public Guid Key { get; set; }
/// <summary>
/// Значение уставки
/// </summary>
public required object Value { get; set; }
} }

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

View File

@ -1,11 +1,8 @@
using System; namespace Persistence.Models;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models; /// <summary>
/// Модель, описывающая пользователя
/// </summary>
public class UserDto public class UserDto
{ {
/// <inheritdoc/> /// <inheritdoc/>

View File

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

View File

@ -7,7 +7,7 @@ namespace Persistence.Repositories;
/// </summary> /// </summary>
/// <typeparam name="TDto"></typeparam> /// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository<TDto, TChangeLogDto> : ISyncRepository<TDto> public interface IChangeLogRepository<TDto, TChangeLogDto> : ISyncRepository<TDto>
where TDto : class, IChangeLogAbstract, new() where TDto : class, ITimeSeriesAbstractDto, new()
where TChangeLogDto : ChangeLogDto<TDto> where TChangeLogDto : ChangeLogDto<TDto>
{ {
/// <summary> /// <summary>

View File

@ -1,10 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models; using Persistence.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Repositories; namespace Persistence.Repositories;
@ -13,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>> GetHistoryAsync(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>>> GetLogAsync(IEnumerable<Guid> setpoitKeys, CancellationToken token); Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token);
/// <summary> /// <summary>
/// Метод сохранения уставки /// Метод сохранения уставки
@ -41,5 +42,5 @@ public interface ISetpointRepository
/// <returns></returns> /// <returns></returns>
/// to do /// to do
/// id User учесть в соответствующем методе репозитория /// id User учесть в соответствующем методе репозитория
Task<int> SaveAsync(Guid setpointKey, int idUser, object newValue, CancellationToken token); Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token);
} }

View File

@ -15,5 +15,5 @@ public interface ITableDataRepository<TDto, TRequest>
/// <param name="request">параметры фильтрации</param> /// <param name="request">параметры фильтрации</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TDto>> GetAsync(TRequest request, CancellationToken token); Task<IEnumerable<TDto>> Get(TRequest request, CancellationToken token);
} }

View File

@ -5,22 +5,25 @@ namespace Persistence.Repositories;
/// <summary> /// <summary>
/// Интерфейс по работе с прореженными данными /// Интерфейс по работе с прореженными данными
/// </summary> /// </summary>
public interface IGraphDataRepository<TDto> public interface ITimeSeriesBaseRepository<TDto>
where TDto : class, new() where TDto : class, new()
{ {
/// <summary> /// <summary>
/// Получить список объектов с прореживанием /// Получить список объектов с прореживанием
/// </summary> /// </summary>
/// <param name="dateBegin">дата начала</param> /// <param name="dateBegin">дата начала</param>
/// <param name="dateEnd">дата окончания</param>
/// <param name="approxPointsCount"></param> /// <param name="approxPointsCount"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<TDto>> GetThinnedDataAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, int approxPointsCount = 1024); Task<IEnumerable<TDto>> GetResampledData(
DateTimeOffset dateBegin,
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default);
/// <summary> /// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории /// Получить диапазон дат, для которых есть данные в репозитории
/// </summary> /// </summary>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token); Task<DatesRangeDto?> GetDatesRange(CancellationToken token);
} }

View File

@ -6,24 +6,9 @@ namespace Persistence.Repositories;
/// Интерфейс по работе с временными данными /// Интерфейс по работе с временными данными
/// </summary> /// </summary>
/// <typeparam name="TDto"></typeparam> /// <typeparam name="TDto"></typeparam>
public interface ITimeSeriesDataRepository<TDto> : ISyncRepository<TDto> public interface ITimeSeriesDataRepository<TDto> : ISyncRepository<TDto>, ITimeSeriesBaseRepository<TDto>
where TDto : class, ITimeSeriesAbstractDto, new() where TDto : class, ITimeSeriesAbstractDto, new()
{ {
/// <summary>
/// Получить страницу списка объектов
/// </summary>
/// <param name="dateBegin">дата начала</param>
/// <param name="dateEnd">дата окончания</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
/// <summary> /// <summary>
/// Добавление записей /// Добавление записей
/// </summary> /// </summary>

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

View File

@ -1,12 +1,12 @@
namespace Persistence.Services; namespace Persistence.Services;
/// <summary> /// <summary>
/// /// Сервис по работе с БД
/// </summary> /// </summary>
internal interface IArchiveService internal interface IArchiveService
{ {
/// <summary> /// <summary>
/// /// Переименование БД
/// </summary> /// </summary>
/// <param name="connectionString"></param> /// <param name="connectionString"></param>
/// <param name="databaseName"></param> /// <param name="databaseName"></param>
@ -15,7 +15,7 @@ internal interface IArchiveService
Task RenameDatabase(string connectionString, string databaseName, CancellationToken token); Task RenameDatabase(string connectionString, string databaseName, CancellationToken token);
/// <summary> /// <summary>
/// /// Создание БД
/// </summary> /// </summary>
/// <param name="connectionString"></param> /// <param name="connectionString"></param>
/// <param name="databaseName"></param> /// <param name="databaseName"></param>

View File

@ -1,4 +1,5 @@
namespace Persistence.Services; namespace Persistence.Services;
public interface ITimeSeriesDataObserverService public interface ITimeSeriesDataObserverService
{ {
} }