Тестовые проект(ы) для реализации интерфейсов Persistence:

- Persistence.API
- Persistence.Database
- Persistence.Repository
- Persistence.IntegrationTests
This commit is contained in:
Olga Nemt 2024-11-14 15:17:43 +05:00
parent 3c94d55caf
commit 828864c112
46 changed files with 1173 additions and 189 deletions

30
.dockerignore Normal file
View File

@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.API;
using Persistence.Repositories;
using Persistence.Repository.Data;
namespace Persistence.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class DataSaubController : TimeSeriesController<DataSaubDto>
{
public DataSaubController(ITimeSeriesDataRepository<DataSaubDto> timeSeriesDataRepository) : base(timeSeriesDataRepository)
{
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TimeSeriesController<TDto> : ControllerBase, ITimeSeriesDataApi<TDto>
where TDto : class, ITimeSeriesAbstractDto, new()
{
private ITimeSeriesDataRepository<TDto> timeSeriesDataRepository;
public TimeSeriesController(ITimeSeriesDataRepository<TDto> timeSeriesDataRepository)
{
this.timeSeriesDataRepository = timeSeriesDataRepository;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var result = await this.timeSeriesDataRepository.GetAsync(dateBegin, dateEnd, token);
return Ok(result);
}
[HttpGet("datesRange")]
public Task<IActionResult> GetDatesRangeAsync(CancellationToken token)
{
throw new NotImplementedException();
}
[HttpPost]
public async Task<IActionResult> InsertRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
{
var result = await this.timeSeriesDataRepository.InsertRange(dtos, token);
return Ok(result);
}
}

View File

@ -0,0 +1,24 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Persistence.API/Persistence.API.csproj", "Persistence.API/"]
RUN dotnet restore "./Persistence.API/Persistence.API.csproj"
COPY . .
WORKDIR "/src/Persistence.API"
RUN dotnet build "./Persistence.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Persistence.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Persistence.API.dll"]

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence.Repository\Persistence.Repository.csproj" />
<ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@Persistence.API_HostAddress = http://localhost:5032
GET {{Persistence.API_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Persistence.Repositories;
using Persistence.Repository;
using Persistence.Repository.Data;
using Persistence.Repository.Repositories;
namespace Persistence.API;
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
Persistence.Repository.Startup.BeforeRunHandler(host);
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

View File

@ -0,0 +1,40 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5032"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:13616",
"sslPort": 0
}
}
}

View File

@ -0,0 +1,40 @@
using Persistence.Repository;
namespace Persistence.API;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
// Add services to the container.
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddInfrastructure(Configuration);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseRouting();
//app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,8 @@
{
"DbConnection": {
"Host": "localhost",
"Port": 5432,
"Username": "postgres",
"Password": "q"
}
}

View File

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True",
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database;
public static class EFExtensionsInitialization
{
public static void EnsureCreatedAndMigrated(this DatabaseFacade db)
{
var connectionString = db.GetConnectionString();
Trace.TraceInformation($"connectionString: {connectionString}");
db.SetCommandTimeout(TimeSpan.FromMinutes(5));
if (db.EnsureCreated())
{
Trace.TraceInformation("Creating DB");
Console.WriteLine("Creating DB");
db.CreateMigrationTable();
db.WriteMigrationsInfo();
}
else
{
Trace.TraceInformation("Migrating DB");
db.SetCommandTimeout(TimeSpan.FromMinutes(20));
Console.WriteLine("db.Migrate()");
db.Migrate();
}
}
private static void CreateMigrationTable(this DatabaseFacade db)
{
var sqlCreateMigrationTable =
$"CREATE TABLE public.\"__EFMigrationsHistory\" " +
$"(\"MigrationId\" varchar(150) NOT NULL, " +
$" \"ProductVersion\" varchar(32) NOT NULL, " +
$" CONSTRAINT \"PK___EFMigrationsHistory\" PRIMARY KEY (\"MigrationId\"));";
db.ExecuteSqlRaw(sqlCreateMigrationTable);
}
private static void WriteMigrationsInfo(this DatabaseFacade db)
{
var efVersion = db.GetType().Assembly.GetName().Version!;
var efVersionString = $"{efVersion.Major}.{efVersion.Minor}.{efVersion.Build}";
var migrations = db.GetPendingMigrations()
.Select(migration => $" ('{migration}', '{efVersionString}')");
var sqlAddLastMigration =
$"INSERT INTO public.\"__EFMigrationsHistory\" " +
$"(\"MigrationId\", \"ProductVersion\") " +
$"VALUES {string.Join(',', migrations)};";
db.ExecuteSqlRaw(sqlAddLastMigration);
}
}

View File

@ -0,0 +1,65 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Persistence.Database.Model;
public class DataSaub : ITimestampedData
{
[Column("id")]
public int Id { get; set; }
[Column("timestamp")]
public int TimeStamp { get; set; }
[Column("mode")]
public int? Mode { get; set; }
[Column("user")]
public string? User { get; set; }
[Column("wellDepth")]
public double? WellDepth { get; set; }
[Column("bitDepth")]
public double? BitDepth { get; set; }
[Column("blockPosition")]
public double? BlockPosition { get; set; }
[Column("blockSpeed")]
public double? BlockSpeed { get; set; }
[Column("pressure")]
public double? Pressure { get; set; }
[Column("axialLoad")]
public double? AxialLoad { get; set; }
[Column("hookWeight")]
public double? HookWeight { get; set; }
[Column("rotorTorque")]
public double? RotorTorque { get; set; }
[Column("rotorSpeed")]
public double? RotorSpeed { get; set; }
[Column("flow")]
public double? Flow { get; set; }
[Column("mseState")]
public short MseState { get; set; }
[Column("idFeedRegulator")]
public int IdFeedRegulator { get; set; }
[Column("mse")]
public double? Mse { get; set; }
[Column("pump0Flow")]
public double? Pump0Flow { get; set; }
[Column("pump1Flow")]
public double? Pump1Flow { get; set; }
[Column("pump2Flow")]
public double? Pump2Flow { get; set; }
}

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,16 @@
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; }
DatabaseFacade Database { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
public interface ITimestampedData
{
int TimeStamp { get; set; }
}

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext, IPersistenceDbContext
{
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
optionsBuilder.UseNpgsql("Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True;Include Error Detail=True;"
//, builder=>builder.EnableRetryOnFailure(2, System.TimeSpan.FromMinutes(1))
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("adminpack")
.HasAnnotation("Relational:Collation", "Russian_Russia.1251");
}
}

View File

@ -0,0 +1,17 @@
<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>
</Project>

View File

@ -0,0 +1,43 @@
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

@ -0,0 +1,30 @@
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Persistence.IntegrationTests;
public abstract class BaseIntegrationTest : IClassFixture<WebAppFactoryFixture>,
IDisposable
{
protected readonly IServiceScope scope;
protected readonly PersistenceDbContext dbContext;
protected BaseIntegrationTest(WebAppFactoryFixture factory)
{
//scope = factory.Services.CreateScope();
//dbContext = scope.ServiceProvider.GetRequiredService<PersistenceDbContext>();
}
public void Dispose()
{
scope.Dispose();
dbContext.Dispose();
}
}

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,45 @@
using Persistence.Repository.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Persistence.IntegrationTests.Controllers;
public class DataSaubControllerTest : TimeSeriesBaseControllerTest<DataSaubDto>
{
private readonly DataSaubDto dto = new DataSaubDto()
{
AxialLoad = 1,
BitDepth = 2,
BlockPosition = 3,
BlockSpeed = 4,
Date = DateTimeOffset.Now,
Flow = 5,
HookWeight = 6,
Id = 7,
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)
{
}
[Fact]
public async Task InsertRange_returns_success()
{
await InsertRangeSuccess(dto);
}
}

View File

@ -0,0 +1,51 @@
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.Text;
using System.Threading.Tasks;
using Xunit;
namespace Persistence.IntegrationTests.Controllers;
public abstract class TimeSeriesBaseControllerTest<TDto> : BaseIntegrationTest
where TDto : class, new()
{
private ITimeSeriesClient<TDto> client;
public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory)
{
//dbContext.CleanupDbSet<TEntity>();
client = factory.GetHttpClient<ITimeSeriesClient<TDto>>(string.Empty);
}
public async Task InsertRangeSuccess(TDto dto)
{
//arrange
var expected = dto.Adapt<TDto>();
//act
var response = await client.InsertRangeAsync(new TDto[] { expected });
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
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);
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.IntegrationTests;
public class DbConnection
{
public string Host { get; set; } = null!;
public int Port { get; set; }
public string Username { get; set; } = null!;
public string Password { get; set; } = null!;
public string GetConnectionString() =>
$"Host={Host};Database={Guid.NewGuid()};Port={Port};Username={Username};Password={Password};Persist Security Info=True;Include Error Detail=True";
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence.API\Persistence.API.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model;
using Persistence.API;
using Refit;
using System.Net.Http.Headers;
using System.Text.Json;
namespace Persistence.IntegrationTests;
public class WebAppFactoryFixture : WebApplicationFactory<Startup>
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
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)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<PersistenceDbContext>(options =>
options.UseNpgsql(connectionString));
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
//dbContext.Database.EnsureCreatedAndMigrated();
//dbContext.Deposits.AddRange(Data.Defaults.Deposits);
dbContext.SaveChanges();
});
}
public override async ValueTask DisposeAsync()
{
var dbContext = new PersistenceDbContext(
new DbContextOptionsBuilder<PersistenceDbContext>()
.UseNpgsql(connectionString)
.Options);
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,46 @@
using Persistence.Models;
using System.ComponentModel.DataAnnotations.Schema;
namespace Persistence.Repository.Data;
public class DataSaubDto : ITimeSeriesAbstractDto
{
public int Id { get; set; }
public DateTimeOffset Date { get; set; } = DateTimeOffset.UtcNow;
public int? Mode { get; set; }
public string? User { get; set; }
public double? WellDepth { get; set; }
public double? BitDepth { get; set; }
public double? BlockPosition { get; set; }
public double? BlockSpeed { get; set; }
public double? Pressure { get; set; }
public double? AxialLoad { get; set; }
public double? HookWeight { get; set; }
public double? RotorTorque { get; set; }
public double? RotorSpeed { get; set; }
public double? Flow { get; set; }
public short MseState { get; set; }
public int IdFeedRegulator { get; set; }
public double? Mse { get; set; }
public double? Pump0Flow { get; set; }
public double? Pump1Flow { get; set; }
public double? Pump2Flow { get; set; }
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Repositories;
using Persistence.Database.Model;
using Persistence.Repository.Data;
using Persistence.Repository.Repositories;
namespace Persistence.Repository;
public static class DependencyInjection
{
public static void MapsterSetup()
{
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
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>>();
return services;
}
}

View File

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

View File

@ -0,0 +1,50 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Persistence.Models;
using Persistence.Repositories;
using Persistence.Database.Model;
using Persistence.Repository.Data;
namespace Persistence.Repository.Repositories;
public abstract class TimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataRepository<TDto>
where TEntity : class
where TDto : class, ITimeSeriesAbstractDto, new()
{
private DbContext db;
public TimeSeriesDataRepository(DbContext db)
{
this.db = db;
}
protected virtual IQueryable<TEntity> GetQueryReadOnly() => db.Set<TEntity>();
public async Task<IEnumerable<TDto>> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<TDto>());
return dtos;
}
public Task<DatesRangeDto> GetDatesRangeAsync(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>());
await db.Set<TEntity>().AddRangeAsync(entities, token);
var result = await db.SaveChangesAsync(token);
return result;
}
}

View File

@ -0,0 +1,19 @@
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

@ -3,7 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34714.143
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence", "Persistence\Persistence.csproj", "{417177AE-A27E-445B-B3B9-D5EFCEC812A0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "Persistence\Persistence.csproj", "{417177AE-A27E-445B-B3B9-D5EFCEC812A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.API", "Persistence.API\Persistence.API.csproj", "{8650A227-929E-45F0-AEF7-2C91F45FE884}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Repository", "Persistence.Repository\Persistence.Repository.csproj", "{493D6D92-231B-4CB6-831B-BE13884B0DE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Persistence.Database\Persistence.Database.csproj", "{F77475D1-D074-407A-9D69-2FADDDAE2056}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -15,6 +23,22 @@ Global
{417177AE-A27E-445B-B3B9-D5EFCEC812A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{417177AE-A27E-445B-B3B9-D5EFCEC812A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{417177AE-A27E-445B-B3B9-D5EFCEC812A0}.Release|Any CPU.Build.0 = Release|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8650A227-929E-45F0-AEF7-2C91F45FE884}.Release|Any CPU.Build.0 = Release|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{493D6D92-231B-4CB6-831B-BE13884B0DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F77475D1-D074-407A-9D69-2FADDDAE2056}.Release|Any CPU.Build.0 = Release|Any CPU
{10752C25-3773-4081-A1F2-215A1D950126}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -6,7 +6,7 @@ namespace Persistence.API;
/// <summary>
/// Интерфейс для работы с API журнала изменений
/// </summary>
public interface IApiChangeLog<TDto, TChangeLogDto>
public interface IChangeLogApi<TDto, TChangeLogDto>
where TDto : class, new()
where TChangeLogDto : ChangeLogDto<TDto>
{

View File

@ -5,7 +5,7 @@ namespace Persistence.API;
/// <summary>
/// Интерфейс для API, предназначенного для работы с элементами справочников
/// </summary>
public interface IApiDictionaryElement<TDto> where TDto : class, new()
public interface IDictionaryElementApi<TDto> where TDto : class, new()
{
/// <summary>
/// Получить все данные справочника

View File

@ -6,7 +6,7 @@ namespace Persistence.API;
/// <summary>
/// Интерфейс для работы с API графиков
/// </summary>
public interface IGraphData<TDto>
public interface IGraphDataApi<TDto>
{
/// <summary>
/// Получить список объектов с прореживанием, удовлетворящий диапазону дат

View File

@ -6,7 +6,7 @@ namespace Persistence.API;
/// <summary>
/// Интерфейс для API, предназначенного для работы с уставками
/// </summary>
public interface IApiSetpoint
public interface ISetpointApi
{
/// <summary>
/// Получить актуальные значения уставок

View File

@ -6,7 +6,7 @@ namespace Persistence.API;
/// <summary>
/// Интерфейс для API, предназначенного для синхронизации данных
/// </summary>
public interface IApiSync<TDto> where TDto : class, new()
public interface ISyncApi<TDto> where TDto : class, new()
{
/// <summary>
/// Получить порцию записей, начиная с заданной даты

View File

@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace Persistence.API;
/// Интерфейс для API, предназначенного для работы с табличными данными
public interface IApiTableData<TDto, TRequest>
public interface ITableDataApi<TDto, TRequest>
where TDto : class, new()
where TRequest : RequestDto
{

View File

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

View File

@ -4,162 +4,162 @@ using Persistence.Models;
using System.Linq;
namespace Persistence.Repositories;
public abstract class AbstractChangeLogRepository<TEntity, TChangeLogDto, TDto> : IChangeLogRepository<TDto, TChangeLogDto>
where TDto : class, new()
where TEntity : class, IChangeLogAbstract
where TChangeLogDto : ChangeLogDto<TDto>
{
private readonly DbContext dbContext;
//public abstract class AbstractChangeLogRepository<TEntity, TChangeLogDto, TDto> : IChangeLogRepository<TDto, TChangeLogDto>
// where TDto : class, new()
// where TEntity : class, IChangeLogAbstract
// where TChangeLogDto : ChangeLogDto<TDto>
//{
// private readonly DbContext dbContext;
protected AbstractChangeLogRepository(DbContext dbContext)
{
this.dbContext = dbContext;
}
// protected AbstractChangeLogRepository(DbContext dbContext)
// {
// this.dbContext = dbContext;
// }
public abstract TEntity Convert(TDto entity);
public async Task<int> Clear(int idUser,CancellationToken token)
{
throw new NotImplementedException();
// public abstract TEntity Convert(TDto entity);
// public async Task<int> Clear(int idUser,CancellationToken token)
// {
// throw new NotImplementedException();
//var updateTime = DateTimeOffset.UtcNow;
// //var updateTime = DateTimeOffset.UtcNow;
////todo
//var query = BuildQuery(request);
//query = query.Where(e => e.Obsolete == null);
// ////todo
// //var query = BuildQuery(request);
// //query = query.Where(e => e.Obsolete == null);
//var entitiesToDelete = await query.ToArrayAsync(token);
// //var entitiesToDelete = await query.ToArrayAsync(token);
//foreach (var entity in entitiesToDelete)
//{
// entity.IdState = IChangeLogAbstract.IdCleared;
// entity.Obsolete = updateTime;
// entity.IdEditor = idUser;
//}
// //foreach (var entity in entitiesToDelete)
// //{
// // entity.IdState = IChangeLogAbstract.IdCleared;
// // entity.Obsolete = updateTime;
// // entity.IdEditor = idUser;
// //}
//var result = await SaveChangesWithExceptionHandling(token);
//return result;
}
// //var result = await SaveChangesWithExceptionHandling(token);
// //return result;
// }
public async Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await dbContext.Database.BeginTransactionAsync(token);
try
{
result += await Clear(idUser, token);
result += await InsertRangeWithoutTransaction(idUser, dtos, token);
// public async Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// var result = 0;
// using var transaction = await dbContext.Database.BeginTransactionAsync(token);
// try
// {
// result += await Clear(idUser, token);
// result += await InsertRangeWithoutTransaction(idUser, dtos, token);
await transaction.CommitAsync(token);
return result;
}
catch
{
await transaction.RollbackAsync(token);
throw;
}
}
// await transaction.CommitAsync(token);
// return result;
// }
// catch
// {
// await transaction.RollbackAsync(token);
// throw;
// }
// }
public Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token)
{
throw new NotImplementedException();
}
// public Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token)
// {
// throw new NotImplementedException();
// }
public Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token)
{
throw new NotImplementedException();
}
// public Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token)
// {
// throw new NotImplementedException();
// }
public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
{
throw new NotImplementedException();
}
// public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
// {
// throw new NotImplementedException();
// }
public async Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
using var transaction = dbContext.Database.BeginTransaction();
try
{
var result = await InsertRangeWithoutTransaction(idUser, dtos, token);
await transaction.CommitAsync(token);
return result;
}
catch
{
await transaction.RollbackAsync(token);
throw;
}
}
// public async Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// using var transaction = dbContext.Database.BeginTransaction();
// try
// {
// var result = await InsertRangeWithoutTransaction(idUser, dtos, token);
// await transaction.CommitAsync(token);
// return result;
// }
// catch
// {
// await transaction.RollbackAsync(token);
// throw;
// }
// }
protected abstract DatabaseFacade GetDataBase();
// protected abstract DatabaseFacade GetDataBase();
public Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, 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> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// throw new NotImplementedException();
// }
public Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
// public Task<int> UpdateRange(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<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token)
// {
// throw new NotImplementedException();
// }
private async Task<int> InsertRangeWithoutTransaction(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
var result = 0;
if (dtos.Any())
{
var entities = dtos.Select(Convert);
var creation = DateTimeOffset.UtcNow;
var dbSet = dbContext.Set<TEntity>();
foreach (var entity in entities)
{
entity.Id = default;
entity.IdAuthor = idUser;
entity.Creation = creation;
entity.IdState = IChangeLogAbstract.IdStateActual;
entity.IdEditor = null;
entity.IdPrevious = null;
entity.Obsolete = null;
dbSet.Add(entity);
}
// private async Task<int> InsertRangeWithoutTransaction(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
// {
// var result = 0;
// if (dtos.Any())
// {
// var entities = dtos.Select(Convert);
// var creation = DateTimeOffset.UtcNow;
// var dbSet = dbContext.Set<TEntity>();
// foreach (var entity in entities)
// {
// entity.Id = default;
// entity.IdAuthor = idUser;
// entity.Creation = creation;
// entity.IdState = IChangeLogAbstract.IdStateActual;
// entity.IdEditor = null;
// entity.IdPrevious = null;
// entity.Obsolete = null;
// dbSet.Add(entity);
// }
result += await SaveChangesWithExceptionHandling(token);
}
// result += await SaveChangesWithExceptionHandling(token);
// }
return result;
}
// return result;
// }
private async Task<int> SaveChangesWithExceptionHandling(CancellationToken token)
{
var result = await dbContext.SaveChangesAsync(token);
return result;
//try
//{
// var result = await dbContext.SaveChangesAsync(token);
// return result;
//}
//catch (DbUpdateException ex)
//{
// if (ex.InnerException is PostgresException pgException)
// TryConvertPostgresExceptionToValidateException(pgException);
// throw;
//}
}
// private async Task<int> SaveChangesWithExceptionHandling(CancellationToken token)
// {
// var result = await dbContext.SaveChangesAsync(token);
// return result;
// //try
// //{
// // var result = await dbContext.SaveChangesAsync(token);
// // return result;
// //}
// //catch (DbUpdateException ex)
// //{
// // if (ex.InnerException is PostgresException pgException)
// // TryConvertPostgresExceptionToValidateException(pgException);
// // throw;
// //}
// }
//private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
//{
// if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
// throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
//}
}
// //private static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
// //{
// // if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
// // throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
// //}
//}

View File

@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Repositories;
public abstract class AbstractTimeSeriesDataRepository<TEntity, TDto> : ITimeSeriesDataRepository<TDto>
where TDto : class, ITimeSeriesAbstractDto
where TEntity : class, IChangeLogAbstract
{
private readonly DbContext dbContext;
protected AbstractTimeSeriesDataRepository(DbContext dbContext)
{
this.dbContext = dbContext;
}
public Task<IEnumerable<TDto>> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> InsertRange(IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
}

View File

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

View File

@ -5,6 +5,7 @@
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface ISyncRepository<TDto>
where TDto : class, new()
{
/// <summary>
/// Получить данные, начиная с определенной даты

View File

@ -7,7 +7,7 @@ namespace Persistence.Repositories;
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface ITimeSeriesDataRepository<TDto> : ISyncRepository<TDto>
where TDto : class, ITimeSeriesAbstractDto
where TDto : class, ITimeSeriesAbstractDto, new()
{
/// <summary>
/// Получить страницу списка объектов