Merge branch 'master' into WitsData

This commit is contained in:
Roman Efremov 2024-12-10 17:15:14 +05:00
commit cdd7aee8e9
51 changed files with 2002 additions and 645 deletions

View File

@ -0,0 +1,178 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using System.Net;
namespace Persistence.API.Controllers;
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class ChangeLogController : ControllerBase, IChangeLogApi
{
private IChangeLogRepository repository;
public ChangeLogController(IChangeLogRepository repository)
{
this.repository = repository;
}
[HttpPost("{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> Add(
[FromRoute] Guid idDiscriminator,
[FromBody] DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, [dto], token);
return CreatedAtAction(nameof(Add), result);
}
[HttpPost("range/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.Created)]
public async Task<IActionResult> AddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.AddRange(userId, idDiscriminator, dtos, token);
return CreatedAtAction(nameof(AddRange), result);
}
[HttpDelete]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Delete(Guid id, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, [id], token);
return Ok(result);
}
[HttpDelete("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.MarkAsDeleted(userId, ids, token);
return Ok(result);
}
[HttpPost("replace/{idDiscriminator}")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ClearAndAddRange(
[FromRoute] Guid idDiscriminator,
[FromBody] IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.ClearAndAddRange(userId, idDiscriminator, dtos, token);
return Ok(result);
}
[HttpPut]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Update(
DataWithWellDepthAndSectionDto dto,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, [dto], token);
return Ok(result);
}
[HttpPut("range")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateRange(
IEnumerable<DataWithWellDepthAndSectionDto> dtos,
CancellationToken token)
{
var userId = User.GetUserId<Guid>();
var result = await repository.UpdateRange(userId, dtos, token);
return Ok(result);
}
[HttpGet("{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCurrent(
[FromRoute] Guid idDiscriminator,
[FromQuery] SectionPartRequest filterRequest,
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var moment = new DateTimeOffset(3000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
[HttpGet("moment/{idDiscriminator}")]
[ProducesResponseType(typeof(PaginationContainer<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetByDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset moment,
[FromQuery] SectionPartRequest filterRequest,
[FromQuery] PaginationRequest paginationRequest,
CancellationToken token)
{
var result = await repository.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest, token);
return Ok(result);
}
[HttpGet("history/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<ChangeLogDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetChangeLogForDate(
[FromRoute] Guid idDiscriminator,
DateTimeOffset dateBegin,
DateTimeOffset dateEnd,
CancellationToken token)
{
var result = await repository.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd, token);
return Ok(result);
}
[HttpGet("datesChange/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DateOnly>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesChange([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesChange(idDiscriminator, token);
return Ok(result);
}
[HttpGet("part/{idDiscriminator}")]
[ProducesResponseType(typeof(IEnumerable<DataWithWellDepthAndSectionDto>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetPart([FromRoute] Guid idDiscriminator, DateTimeOffset dateBegin, int take = 86400, CancellationToken token = default)
{
var result = await repository.GetGtDate(idDiscriminator, dateBegin, token);
return Ok(result);
}
[HttpGet("datesRange/{idDiscriminator}")]
[ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetDatesRangeAsync([FromRoute] Guid idDiscriminator, CancellationToken token)
{
var result = await repository.GetDatesRange(idDiscriminator, token);
if (result is null)
return NoContent();
return Ok(result);
}
}

View File

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
namespace Persistence.API.Controllers;
@ -36,7 +37,7 @@ public class TechMessagesController : ControllerBase
/// <param name="token"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<PaginationContainer<TechMessageDto>>> GetPage([FromQuery] RequestDto request, CancellationToken token)
public async Task<ActionResult<PaginationContainer<TechMessageDto>>> GetPage([FromQuery] PaginationRequest request, CancellationToken token)
{
var result = await techMessagesRepository.GetPage(request, token);

View File

@ -1,5 +1,3 @@
using System.Reflection;
using System.Text.Json.Nodes;
using Mapster;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
@ -11,17 +9,19 @@ using Persistence.Models.Configurations;
using Persistence.Services;
using Persistence.Services.Interfaces;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Text.Json.Nodes;
namespace Persistence.API;
public static class DependencyInjection
{
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
}
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TechMessageDto, TechMessage>()
.Ignore(dest => dest.System, dest => dest.SystemId);
}
public static void AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(c =>
@ -45,16 +45,16 @@ public static class DependencyInjection
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 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;
c.IncludeXmlComments(xmlPath, includeControllerXmlComment);
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
var includeControllerXmlComment = true;
c.IncludeXmlComments(xmlPath, includeControllerXmlComment);
});
}
public static void AddServices(this IServiceCollection services)
@ -66,146 +66,146 @@ public static class DependencyInjection
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);
}
.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 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();
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;
context.Token = accessToken;
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var username = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "username")?.Value;
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 password = context.Principal?.Claims
.FirstOrDefault(e => e.Type == "password")?.Value;
var keyCloakUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
var keyCloakUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
if (username != keyCloakUser.Username || password != keyCloakUser.Password)
{
context.Fail("username or password did not match");
}
if (username != keyCloakUser.Username || password != keyCloakUser.Password)
{
context.Fail("username or password did not match");
}
return Task.CompletedTask;
}
};
});
}
#endregion
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"]),
}
}
});
#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>()
}
});
}
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",
});
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
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

@ -22,6 +22,7 @@ public class Startup
services.AddSwagger(Configuration);
services.AddInfrastructure();
services.AddPersistenceDbContext(Configuration);
services.AddAuthorization();
services.AddJWTAuthentication(Configuration);
services.AddMemoryCache();
services.AddServices();
@ -41,6 +42,9 @@ public class Startup
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
@ -55,7 +59,7 @@ public class Startup
using var scope = host.Services.CreateScope();
var provider = scope.ServiceProvider;
var context = provider.GetRequiredService<PersistenceDbContext>();
var context = provider.GetRequiredService<PersistencePostgresContext>();
context.Database.EnsureCreatedAndMigrated();
}

View File

@ -14,5 +14,13 @@
"Audience": "account",
"ValidIssuer": "http://192.168.0.10:8321/realms/Persistence",
"AuthorizationUrl": "http://192.168.0.10:8321/realms/Persistence/protocol/openid-connect/auth"
},
"NeedUseKeyCloak": false,
"AuthUser": {
"username": "myuser",
"password": 12345,
"clientId": "webapi",
"grantType": "password",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "7d9f3574-6574-4ca3-845a-0276eb4aa8f6"
}
}

View File

@ -0,0 +1,106 @@
using Persistence.Models;
using Persistence.Models.Requests;
using Refit;
namespace Persistence.Client.Clients;
/// <summary>
/// Интерфейс для тестирования API, предназначенного для работы с записями ChangeLod
/// </summary>
public interface IChangeLogClient
{
private const string BaseRoute = "/api/ChangeLog";
/// <summary>
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <returns></returns>
[Post($"{BaseRoute}/replace/{{idDiscriminator}}")]
Task<IApiResponse<int>> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Получение актуальных данных на определенную дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <returns></returns>
[Get($"{BaseRoute}/moment/{{idDiscriminator}}")]
Task<IApiResponse<PaginationContainer<DataWithWellDepthAndSectionDto>>> GetByDate(
Guid idDiscriminator,
DateTimeOffset moment,
[Query] SectionPartRequest filterRequest,
[Query] PaginationRequest paginationRequest);
/// <summary>
/// Получение исторических данных за определенный период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <returns></returns>
[Get($"{BaseRoute}/history/{{idDiscriminator}}")]
Task<IApiResponse<IEnumerable<ChangeLogDto>>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <returns></returns>
[Post($"{BaseRoute}/{{idDiscriminator}}")]
Task<IApiResponse<int>> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <returns></returns>
[Post($"{BaseRoute}/range/{{idDiscriminator}}")]
Task<IApiResponse<int>> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Обновить одну запись
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Put($"{BaseRoute}")]
Task<IApiResponse<int>> Update(DataWithWellDepthAndSectionDto dto);
/// <summary>
/// Обновить несколько записей
/// </summary>
/// <param name="dtos"></param>
/// <returns></returns>
[Put($"{BaseRoute}/range")]
Task<IApiResponse<int>> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos);
/// <summary>
/// Удалить одну запись
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Delete($"{BaseRoute}")]
Task<IApiResponse<int>> Delete(Guid id);
/// <summary>
/// Удалить несколько записей
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[Delete($"{BaseRoute}/range")]
Task<IApiResponse<int>> DeleteRange([Body] IEnumerable<Guid> ids);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <returns></returns>
[Get($"{BaseRoute}/datesRange/{{idDiscriminator}}")]
Task<IApiResponse<DatesRangeDto?>> GetDatesRange(Guid idDiscriminator);
}

View File

@ -1,4 +1,5 @@
using Persistence.Models;
using Persistence.Models.Requests;
using Refit;
namespace Persistence.Client.Clients
@ -11,7 +12,7 @@ namespace Persistence.Client.Clients
private const string BaseRoute = "/api/techMessages";
[Get($"{BaseRoute}")]
Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] RequestDto request, CancellationToken token);
Task<IApiResponse<PaginationContainer<TechMessageDto>>> GetPage([Query] PaginationRequest request, CancellationToken token);
[Post($"{BaseRoute}")]
Task<IApiResponse<int>> AddRange([Body] IEnumerable<TechMessageDto> dtos, CancellationToken token);

View File

@ -36,7 +36,8 @@ public static class ApiTokenHelper
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType)
new("grant_type", authUser.GrantType),
new(ClaimTypes.NameIdentifier.ToString(), Guid.NewGuid().ToString())
};
var tokenDescriptor = new SecurityTokenDescriptor

View File

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
namespace Persistence.Database.Model;
@ -10,10 +11,10 @@ public static class DependencyInjection
{
string connectionStringName = "DefaultConnection";
services.AddDbContext<PersistenceDbContext>(options =>
services.AddDbContext<PersistencePostgresContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
services.AddScoped<DbContext>(provider => provider.GetRequiredService<PersistenceDbContext>());
services.AddScoped<DbContext>(provider => provider.GetRequiredService<PersistencePostgresContext>());
return services;
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Npgsql;
using Persistence.Database.Model;
namespace Persistence.Database.Postgres;
/// <summary>
/// Фабрика контекста для dotnet ef миграций
/// </summary>
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<PersistencePostgresContext>
{
public PersistencePostgresContext CreateDbContext(string[] args)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder();
connectionStringBuilder.Host = "localhost";
connectionStringBuilder.Database = "persistence";
connectionStringBuilder.Username = "postgres";
connectionStringBuilder.Password = "q";
connectionStringBuilder.PersistSecurityInfo = true;
var connectionString = connectionStringBuilder.ToString();
var optionsBuilder = new DbContextOptionsBuilder<PersistencePostgresContext>();
optionsBuilder.UseNpgsql(connectionString);
var context = new PersistencePostgresContext(optionsBuilder.Options);
return context;
}
}

View File

@ -49,10 +49,13 @@ public static class EFExtensionsInitialization
var migrations = db.GetPendingMigrations()
.Select(migration => $" ('{migration}', '{efVersionString}')");
var sqlAddLastMigration =
$"INSERT INTO public.\"__EFMigrationsHistory\" " +
$"(\"MigrationId\", \"ProductVersion\") " +
$"VALUES {string.Join(',', migrations)};";
db.ExecuteSqlRaw(sqlAddLastMigration);
if (migrations.Any())
{
var sqlAddLastMigration =
$"INSERT INTO public.\"__EFMigrationsHistory\" " +
$"(\"MigrationId\", \"ProductVersion\") " +
$"VALUES {string.Join(',', migrations)};";
db.ExecuteSqlRaw(sqlAddLastMigration);
}
}
}

View File

@ -1,5 +1,6 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -12,8 +13,8 @@ using Persistence.Database.Model;
namespace Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistenceDbContext))]
[Migration("20241122074646_InitialCreate")]
partial class InitialCreate
[Migration("20241126071115_Add_ChangeLog")]
partial class Add_ChangeLog
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -27,6 +28,59 @@ namespace Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasColumnName("DepthEnd");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasColumnName("DepthStart");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasColumnName("IdAuthor");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasColumnName("IdDiscriminator");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasColumnName("IdEditor");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasColumnName("IdNext");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasColumnName("IdSection");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<IDictionary<string, object>>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{
b.Property<DateTimeOffset>("Date")

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class Add_ChangeLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ChangeLog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false),
IdAuthor = table.Column<Guid>(type: "uuid", nullable: false),
IdEditor = table.Column<Guid>(type: "uuid", nullable: true),
Creation = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
Obsolete = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
IdNext = table.Column<Guid>(type: "uuid", nullable: true),
DepthStart = table.Column<double>(type: "double precision", nullable: false),
DepthEnd = table.Column<double>(type: "double precision", nullable: false),
IdSection = table.Column<Guid>(type: "uuid", nullable: false),
Value = table.Column<IDictionary<string, object>>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChangeLog", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChangeLog");
}
}
}

View File

@ -11,9 +11,9 @@ using Persistence.Database.Model;
namespace Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistenceDbContext))]
[Migration("20241118052225_SetpointMigration")]
partial class SetpointMigration
[DbContext(typeof(PersistencePostgresContext))]
[Migration("20241126100631_Init")]
partial class Init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -27,14 +27,34 @@ namespace Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Persistence.Database.Entity.TimestampedSet", b =>
{
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasComment("Дискриминатор ссылка на тип сохраняемых данных");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone")
.HasComment("Отметка времени, строго в UTC");
b.Property<string>("Set")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Набор сохраняемых данных");
b.HasKey("IdDiscriminator", "Timestamp");
b.ToTable("TimestampedSets", t =>
{
t.HasComment("Общая таблица данных временных рядов");
});
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");
b.Property<double?>("AxialLoad")
.HasColumnType("double precision")
@ -100,10 +120,6 @@ namespace Persistence.Database.Postgres.Migrations
.HasColumnType("double precision")
.HasColumnName("rotorTorque");
b.Property<int>("TimeStamp")
.HasColumnType("integer")
.HasColumnName("timestamp");
b.Property<string>("User")
.HasColumnType("text")
.HasColumnName("user");
@ -112,7 +128,7 @@ namespace Persistence.Database.Postgres.Migrations
.HasColumnType("double precision")
.HasColumnName("wellDepth");
b.HasKey("Id");
b.HasKey("Date");
b.ToTable("DataSaub");
});
@ -125,7 +141,7 @@ namespace Persistence.Database.Postgres.Migrations
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasComment("Дата изменения уставки");
.HasComment("Дата создания уставки");
b.Property<int>("IdUser")
.HasColumnType("integer")

View File

@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
@ -42,6 +42,34 @@ namespace Persistence.Database.Postgres.Migrations
{
table.PrimaryKey("PK_DataSaub", x => x.date);
});
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 });
});
migrationBuilder.CreateTable(
name: "TimestampedSets",
columns: table => new
{
IdDiscriminator = table.Column<Guid>(type: "uuid", nullable: false, comment: "Дискриминатор ссылка на тип сохраняемых данных"),
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Отметка времени, строго в UTC"),
Set = table.Column<string>(type: "jsonb", nullable: false, comment: "Набор сохраняемых данных")
},
constraints: table =>
{
table.PrimaryKey("PK_TimestampedSets", x => new { x.IdDiscriminator, x.Timestamp });
},
comment: "Общая таблица данных временных рядов");
}
/// <inheritdoc />
@ -49,6 +77,12 @@ namespace Persistence.Database.Postgres.Migrations
{
migrationBuilder.DropTable(
name: "DataSaub");
migrationBuilder.DropTable(
name: "Setpoint");
migrationBuilder.DropTable(
name: "TimestampedSets");
}
}
}

View File

@ -1,5 +1,6 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -10,8 +11,8 @@ using Persistence.Database.Model;
namespace Persistence.Database.Postgres.Migrations
{
[DbContext(typeof(PersistenceDbContext))]
partial class PersistenceDbContextModelSnapshot : ModelSnapshot
[DbContext(typeof(PersistencePostgresContext))]
partial class PersistencePostgresContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
@ -131,6 +132,59 @@ namespace Persistence.Database.Postgres.Migrations
});
});
modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<double>("DepthEnd")
.HasColumnType("double precision")
.HasColumnName("DepthEnd");
b.Property<double>("DepthStart")
.HasColumnType("double precision")
.HasColumnName("DepthStart");
b.Property<Guid>("IdAuthor")
.HasColumnType("uuid")
.HasColumnName("IdAuthor");
b.Property<Guid>("IdDiscriminator")
.HasColumnType("uuid")
.HasColumnName("IdDiscriminator");
b.Property<Guid?>("IdEditor")
.HasColumnType("uuid")
.HasColumnName("IdEditor");
b.Property<Guid?>("IdNext")
.HasColumnType("uuid")
.HasColumnName("IdNext");
b.Property<Guid>("IdSection")
.HasColumnType("uuid")
.HasColumnName("IdSection");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<IDictionary<string, object>>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{
b.Property<DateTimeOffset>("Date")

View File

@ -1,55 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Entity;
namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext
{
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<TechMessage> TechMessage => Set<TechMessage>();
public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
public DbSet<ParameterData> ParameterData => Set<ParameterData>();
public PersistenceDbContext()
: base()
{
}
public PersistenceDbContext(DbContextOptions<PersistenceDbContext> options)
: base(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");
modelBuilder.Entity<TimestampedSet>()
.Property(e => e.Set)
.HasJsonConversion();
modelBuilder.Entity<TechMessage>(entity =>
{
entity.HasOne(t => t.System)
.WithMany()
.HasForeignKey(t => t.SystemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
namespace Persistence.Database.Model;
/// <summary>
/// EF êîíòåêñò äëÿ ÁÄ Postgres
/// </summary>
public partial class PersistencePostgresContext : PersistenceDbContext
{
public PersistencePostgresContext(DbContextOptions options)
: base(options)
{}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("adminpack")
.HasAnnotation("Relational:Collation", "Russian_Russia.1251");
base.OnModelCreating(modelBuilder);
}
}

View File

@ -3,3 +3,9 @@
dotnet ef migrations add <MigrationName> --project Persistence.Database.Postgres
```
## Откатить миграцию
```
dotnet ef migrations remove --project Persistence.Database.Postgres
```
Удаляется последняя созданная миграция.

View File

@ -0,0 +1,46 @@

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Persistence.Models;
namespace Persistence.Database.Model;
/// <summary>
/// Часть записи, описывающая изменение
/// </summary>
public class ChangeLog : IChangeLog, IWithSectionPart
{
[Key, Comment("Ключ записи")]
public Guid Id { get; set; }
[Comment("Дискриминатор таблицы")]
public Guid IdDiscriminator { get; set; }
[Comment("Автор изменения")]
public Guid IdAuthor { get; set; }
[Comment("Редактор")]
public Guid? IdEditor { get; set; }
[Comment("Дата создания записи")]
public DateTimeOffset Creation { get; set; }
[Comment("Дата устаревания (например при удалении)")]
public DateTimeOffset? Obsolete { get; set; }
[Comment("Id заменяющей записи")]
public Guid? IdNext { get; set; }
[Comment("Глубина забоя на дату начала интервала")]
public double DepthStart { get; set; }
[Comment("Глубина забоя на дату окончания интервала")]
public double DepthEnd { get; set; }
[Comment("Ключ секции")]
public Guid IdSection { get; set; }
[Column(TypeName = "jsonb"), Comment("Значение")]
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -0,0 +1,48 @@

namespace Persistence.Database.Model;
/// <summary>
/// Часть записи, описывающая изменение
/// </summary>
public interface IChangeLog
{
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public Guid IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public Guid? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id заменяющей записи
/// </summary>
public Guid? IdNext { get; set; }
/// <summary>
/// Дискриминатор таблицы
/// </summary>
public Guid IdDiscriminator { get; set; }
/// <summary>
/// Значение
/// </summary>
public IDictionary<string, object> Value { get; set; }
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Database.Model;
namespace Persistence.Database.Model;
public interface ITimestampedData
{
/// <summary>

View File

@ -14,4 +14,8 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Entity;
using Persistence.Database.Model;
namespace Persistence.Database;
/// <summary>
/// EF контекст для любых БД поддерживаемых в EF
/// </summary>
public class PersistenceDbContext : DbContext
{
public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public DbSet<TimestampedSet> TimestampedSets => Set<TimestampedSet>();
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public PersistenceDbContext(DbContextOptions options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TimestampedSet>()
.Property(e => e.Set)
.HasJsonConversion();
modelBuilder.Entity<ChangeLog>()
.Property(e => e.Value)
.HasJsonConversion();
}
}

View File

@ -1,10 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database;
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;
@ -19,7 +16,7 @@ public abstract class BaseIntegrationTest : IClassFixture<WebAppFactoryFixture>,
{
scope = factory.Services.CreateScope();
dbContext = scope.ServiceProvider.GetRequiredService<PersistenceDbContext>();
dbContext = scope.ServiceProvider.GetRequiredService<PersistencePostgresContext>();
}
public void Dispose()

View File

@ -0,0 +1,351 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
using System.Net;
using Xunit;
namespace Persistence.IntegrationTests.Controllers;
public class ChangeLogControllerTest : BaseIntegrationTest
{
private readonly IChangeLogClient client;
private static Random generatorRandomDigits = new Random();
public ChangeLogControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
client = persistenceClientFactory.GetClient<IChangeLogClient>();
}
[Fact]
public async Task ClearAndInsertRange_InEmptyDb()
{
// arrange
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(2, DateTimeOffset.UtcNow);
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
}
[Fact]
public async Task ClearAndInsertRange_InNotEmptyDb()
{
// arrange
var insertedCount = 10;
var createdResult = CreateChangeLogItems(insertedCount, (-15, 15));
var idDiscriminator = createdResult.Item1;
var dtos = createdResult.Item2.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>());
// act
var result = await client.ClearAndAddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(insertedCount*2, result.Content);
}
[Fact]
public async Task Add_returns_success()
{
// arrange
var count = 1;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
// act
var result = await client.Add(idDiscriminator, dto);
// assert
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task AddRange_returns_success()
{
// arrange
var count = 3;
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
// act
var result = await client.AddRange(idDiscriminator, dtos);
// assert
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task Update_returns_success()
{
// arrange
var idDiscriminator = Guid.NewGuid();
var dtos = Generate(1, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
var result = await client.Add(idDiscriminator, dto);
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
var entity = dbContext.ChangeLog
.Where(x => x.IdDiscriminator == idDiscriminator)
.FirstOrDefault();
dto = entity.Adapt<DataWithWellDepthAndSectionDto>();
dto.DepthEnd = dto.DepthEnd + 10;
// act
result = await client.Update(dto);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(2, result.Content);
var dateBegin = DateTimeOffset.UtcNow.AddDays(-1);
var dateEnd = DateTimeOffset.UtcNow.AddDays(1);
var changeLogResult = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd);
Assert.Equal(HttpStatusCode.OK, changeLogResult.StatusCode);
Assert.NotNull(changeLogResult.Content);
var changeLogDtos = changeLogResult.Content;
var obsoleteDto = changeLogDtos
.Where(e => e.Obsolete.HasValue)
.FirstOrDefault();
var activeDto = changeLogDtos
.Where(e => !e.Obsolete.HasValue)
.FirstOrDefault();
if (obsoleteDto == null || activeDto == null)
{
Assert.Fail();
return;
}
Assert.Equal(activeDto.Id, obsoleteDto.IdNext);
}
[Fact]
public async Task UpdateRange_returns_success()
{
// arrange
var count = 2;
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
dtos = entities.Select(c => new DataWithWellDepthAndSectionDto()
{
DepthEnd = c.DepthEnd + 10,
DepthStart = c.DepthStart + 10,
Id = c.Id,
IdSection = c.IdSection,
Value = c.Value
}).ToArray();
// act
var result = await client.UpdateRange(dtos);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(count * 2, result.Content);
}
[Fact]
public async Task Delete_returns_success()
{
// arrange
var dtos = Generate(1, DateTimeOffset.UtcNow);
var dto = dtos.FirstOrDefault()!;
var entity = dto.Adapt<ChangeLog>();
dbContext.ChangeLog.Add(entity);
dbContext.SaveChanges();
// act
var result = await client.Delete(entity.Id);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(1, result.Content);
}
[Fact]
public async Task DeleteRange_returns_success()
{
// arrange
var count = 10;
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d => d.Adapt<ChangeLog>()).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
// act
var ids = entities.Select(e => e.Id);
var result = await client.DeleteRange(ids);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal(count, result.Content);
}
[Fact]
public async Task GetDatesRange_returns_success()
{
// arrange
var changeLogItems = CreateChangeLogItems(3, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2.OrderBy(e => e.Creation);
// act
var result = await client.GetDatesRange(idDiscriminator);
// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
var minDate = entities.First().Creation;
var maxDate = entities.Last().Creation;
var expectedMinDate = minDate.ToUniversalTime().ToString();
var actualMinDate = result.Content.From.ToUniversalTime().ToString();
Assert.Equal(expectedMinDate, actualMinDate);
var expectedMaxDate = maxDate.ToUniversalTime().ToString();
var actualMaxDate = result.Content.To.ToUniversalTime().ToString();
Assert.Equal(expectedMaxDate, actualMaxDate);
}
[Fact]
public async Task GetByDate_returns_success()
{
// arrange
//создаем записи
var count = 5;
var changeLogItems = CreateChangeLogItems(count, (-15, 15));
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
//удаляем все созданные записи за исключением первой и второй
//даты 2-х оставшихся записей должны вернуться в методе GetByDate
var ids = entities.Select(e => e.Id);
var idsToDelete = ids.Skip(2);
var deletedCount = await client.DeleteRange(idsToDelete);
var filterRequest = new SectionPartRequest()
{
DepthStart = 0,
DepthEnd = 1000,
};
var paginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 10,
SortSettings = String.Empty,
};
var moment = DateTimeOffset.UtcNow.AddDays(16);
var result = await client.GetByDate(idDiscriminator, moment, filterRequest, paginationRequest);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
var restEntities = entities.Where(e => !idsToDelete.Contains(e.Id));
Assert.Equal(restEntities.Count(), result.Content.Count);
var actualIds = restEntities.Select(e => e.Id);
var expectedIds = result.Content.Items.Select(e => e.Id);
Assert.Equivalent(expectedIds, actualIds);
}
[Theory]
[InlineData(5, -15, 15, -20, 20, 10)]
[InlineData(5, -15, -10, -16, -9, 5)]
public async Task GetChangeLogForInterval_returns_success(
int insertedCount,
int daysBeforeNowChangeLog,
int daysAfterNowChangeLog,
int daysBeforeNowFilter,
int daysAfterNowFilter,
int changeLogCount)
{
// arrange
//создаем записи
var count = insertedCount;
var daysRange = (daysBeforeNowChangeLog, daysAfterNowChangeLog);
var changeLogItems = CreateChangeLogItems(count, daysRange);
var idDiscriminator = changeLogItems.Item1;
var entities = changeLogItems.Item2;
foreach (var entity in entities)
{
entity.DepthEnd = entity.DepthEnd + 10;
}
var dtos = entities.Select(e => e.Adapt<DataWithWellDepthAndSectionDto>()).ToArray();
await client.UpdateRange(dtos);
//act
var dateBegin = DateTimeOffset.UtcNow.AddDays(daysBeforeNowFilter);
var dateEnd = DateTimeOffset.UtcNow.AddDays(daysAfterNowFilter);
var result = await client.GetChangeLogForInterval(idDiscriminator, dateBegin, dateEnd);
//assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.NotNull(result.Content);
Assert.Equal(changeLogCount, result.Content.Count());
}
private static IEnumerable<DataWithWellDepthAndSectionDto> Generate(int count, DateTimeOffset from)
{
for (int i = 0; i < count; i++)
yield return new DataWithWellDepthAndSectionDto()
{
Value = new Dictionary<string, object>()
{
{ "Key", 1 }
},
DepthStart = generatorRandomDigits.Next(1, 5),
DepthEnd = generatorRandomDigits.Next(5, 15),
Id = Guid.NewGuid(),
IdSection = Guid.NewGuid()
};
}
private (Guid, ChangeLog[]) CreateChangeLogItems(int count, (int, int) daysRange)
{
var minDayCount = daysRange.Item1;
var maxDayCount = daysRange.Item2;
Guid idDiscriminator = Guid.NewGuid();
var dtos = Generate(count, DateTimeOffset.UtcNow);
var entities = dtos.Select(d =>
{
var entity = d.Adapt<ChangeLog>();
entity.IdDiscriminator = idDiscriminator;
entity.Creation = DateTimeOffset.UtcNow.AddDays(generatorRandomDigits.Next(minDayCount, maxDayCount));
return entity;
}).ToArray();
dbContext.ChangeLog.AddRange(entities);
dbContext.SaveChanges();
return (idDiscriminator, entities);
}
}

View File

@ -1,10 +1,11 @@
using System.Net;
using System.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Entity;
using Persistence.Models;
using Persistence.Models.Requests;
using Xunit;
namespace Persistence.IntegrationTests.Controllers
@ -32,7 +33,7 @@ namespace Persistence.IntegrationTests.Controllers
dbContext.CleanupDbSet<TechMessage>();
dbContext.CleanupDbSet<Database.Entity.DrillingSystem>();
var requestDto = new RequestDto()
var PaginationRequest = new PaginationRequest()
{
Skip = 1,
Take = 2,
@ -46,8 +47,8 @@ namespace Persistence.IntegrationTests.Controllers
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content.Items);
Assert.Equal(requestDto.Skip, response.Content.Skip);
Assert.Equal(requestDto.Take, response.Content.Take);
Assert.Equal(PaginationRequest.Skip, response.Content.Skip);
Assert.Equal(PaginationRequest.Take, response.Content.Take);
}
[Fact]
@ -56,7 +57,7 @@ namespace Persistence.IntegrationTests.Controllers
//arrange
var dtos = await InsertRange();
var dtosCount = dtos.Count();
var requestDto = new RequestDto()
var PaginationRequest = new PaginationRequest()
{
Skip = 0,
Take = 2,

View File

@ -1,10 +1,11 @@
using Persistence.Database.Model;
using Persistence.Database;
using Persistence.Database.Model;
namespace Persistence.IntegrationTests.Extensions;
public static class EFCoreExtensions
{
public static void CleanupDbSet<T>(this PersistenceDbContext dbContext)
public static void CleanupDbSet<T>(this PersistenceDbContext dbContext)
where T : class
{
var dbset = dbContext.Set<T>();

View File

@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Persistence.API;
using Persistence.Database;
using Persistence.Client;
using Persistence.Database.Model;
using Persistence.Database.Postgres;
@ -28,10 +27,11 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistencePostgresContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<PersistenceDbContext>(options =>
services.AddDbContext<PersistencePostgresContext>(options =>
options.UseNpgsql(connectionString));
services.RemoveAll<IHttpClientFactory>();
@ -47,7 +47,7 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
var dbContext = scopedServices.GetRequiredService<PersistencePostgresContext>();
dbContext.Database.EnsureCreatedAndMigrated();
dbContext.SaveChanges();
});
@ -55,8 +55,8 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
public override async ValueTask DisposeAsync()
{
var dbContext = new PersistenceDbContext(
new DbContextOptionsBuilder<PersistenceDbContext>()
var dbContext = new PersistencePostgresContext(
new DbContextOptionsBuilder<PersistencePostgresContext>()
.UseNpgsql(connectionString)
.Options);

View File

@ -1,5 +1,7 @@
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories;
using Persistence.Repository.Data;
using Persistence.Repository.Repositories;
@ -9,6 +11,17 @@ public static class DependencyInjection
{
public static void MapsterSetup()
{
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<ChangeLog, ChangeLogDto>()
.Map(dest => dest.Value, src => new DataWithWellDepthAndSectionDto()
{
DepthEnd = src.DepthEnd,
DepthStart = src.DepthStart,
IdSection = src.IdSection,
Value = src.Value,
Id = src.Id
});
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
@ -18,6 +31,7 @@ public static class DependencyInjection
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ISetpointRepository, SetpointRepository>();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<IChangeLogRepository, ChangeLogRepository>();
services.AddTransient<ITimestampedSetRepository, TimestampedSetRepository>();
services.AddTransient<ITechMessagesRepository, TechMessagesRepository>();
services.AddTransient<IParameterRepository, ParameterRepository>();

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="UuidExtensions" Version="1.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,82 @@
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repository;
/// <summary>
/// класс с набором методов, необходимых для фильтрации записей
/// </summary>
public static class QueryBuilders
{
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> query, SectionPartRequest request)
where TEntity : class, IWithSectionPart
{
if (request.IdSection.HasValue)
{
query = query.Where(e => e.IdSection == request.IdSection);
}
if (request.DepthStart.HasValue)
{
query = query.Where(e => e.DepthStart >= request.DepthStart);
}
if (request.DepthEnd.HasValue)
{
query = query.Where(e => e.DepthEnd <= request.DepthEnd);
}
return query;
}
public static IQueryable<TEntity> Apply<TEntity>(this IQueryable<TEntity> query,DateTimeOffset momentUtc)
where TEntity : class, IChangeLog
{
momentUtc = momentUtc.ToUniversalTime();
query = query
.Where(e => e.Creation <= momentUtc)
.Where(e => e.Obsolete == null || e.Obsolete >= momentUtc);
return query;
}
public static async Task<PaginationContainer<TDto>> ApplyPagination<TEntity, TDto>(
this IQueryable<TEntity> query,
PaginationRequest request,
Func<TEntity, TDto> Convert,
CancellationToken token)
where TEntity : class, IWithSectionPart
where TDto : class
{
if (String.IsNullOrEmpty(request.SortSettings))
{
query = query
.OrderBy(e => e.IdSection)
.ThenBy(e => e.DepthStart)
.ThenBy(e => e.DepthEnd);
}
else
{
query = query.SortBy(request.SortSettings);
}
var entities = await query
.Skip(request.Skip)
.Take(request.Take)
.ToArrayAsync(token);
var count = await query.CountAsync(token);
var items = entities.Select(Convert);
var result = new PaginationContainer<TDto>
{
Skip = request.Skip,
Take = request.Take,
Items = items,
Count = count
};
return result;
}
}

View File

@ -0,0 +1,259 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using UuidExtensions;
namespace Persistence.Repository.Repositories;
public class ChangeLogRepository : IChangeLogRepository
{
private DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public async Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var entities = new List<ChangeLog>();
foreach (var dto in dtos)
{
var entity = CreateEntityFromDto(idAuthor, idDiscriminator, dto);
entities.Add(entity);
}
db.Set<ChangeLog>().AddRange(entities);
var result = await db.SaveChangesAsync(token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => ids.Contains(s.Id))
.Where(s => s.Obsolete == null);
if (query.Count() != ids.Count())
{
throw new ArgumentException("Count of active items not equal count of ids", nameof(ids));
}
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
public async Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(s => s.IdDiscriminator == idDiscriminator)
.Where(e => e.Obsolete == null);
var entities = await query.ToArrayAsync(token);
var result = await MarkAsObsolete(idEditor, entities, token);
return result;
}
private async Task<int> MarkAsObsolete(Guid idEditor, IEnumerable<ChangeLog> entities, CancellationToken token)
{
var updateTime = DateTimeOffset.UtcNow;
foreach (var entity in entities)
{
entity.Obsolete = updateTime;
entity.IdEditor = idEditor;
}
return await db.SaveChangesAsync(token);
}
public async Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
result += await MarkAsDeleted(idAuthor, idDiscriminator, token);
result += await AddRange(idAuthor, idDiscriminator, dtos, token);
await transaction.CommitAsync(token);
return result;
}
public async Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token)
{
var dbSet = db.Set<ChangeLog>();
var updatedIds = dtos.Select(d => d.Id);
var updatedEntities = dbSet
.Where(s => updatedIds.Contains(s.Id))
.ToDictionary(s => s.Id);
var result = 0;
using var transaction = await db.Database.BeginTransactionAsync(token);
foreach (var dto in dtos)
{
var updatedEntity = updatedEntities.GetValueOrDefault(dto.Id);
if (updatedEntity is null)
{
throw new ArgumentException($"Entity with id = {dto.Id} doesn't exist in Db", nameof(dto));
}
var newEntity = CreateEntityFromDto(idEditor, updatedEntity.IdDiscriminator, dto);
dbSet.Add(newEntity);
updatedEntity.IdNext = newEntity.Id;
updatedEntity.Obsolete = DateTimeOffset.UtcNow;
updatedEntity.IdEditor = idEditor;
}
result = await db.SaveChangesAsync(token);
await transaction.CommitAsync(token);
return result;
}
public async Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(
Guid idDiscriminator,
DateTimeOffset momentUtc,
SectionPartRequest filterRequest,
PaginationRequest paginationRequest,
CancellationToken token)
{
var query = CreateQuery(idDiscriminator);
query = query.Apply(momentUtc);
query = query.Apply(filterRequest);
var result = await query.ApplyPagination(paginationRequest, Convert, token);
return result;
}
private IQueryable<ChangeLog> CreateQuery(Guid idDiscriminator)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
return query;
}
public async Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(s => s.IdDiscriminator == idDiscriminator);
var min = new DateTimeOffset(dateBegin.ToUniversalTime().Date, TimeSpan.Zero);
var max = new DateTimeOffset(dateEnd.ToUniversalTime().Date, TimeSpan.Zero);
var createdQuery = query.Where(e => e.Creation >= min && e.Creation <= max);
var editedQuery = query.Where(e => e.Obsolete != null && e.Obsolete >= min && e.Obsolete <= max);
query = createdQuery.Union(editedQuery);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(e => e.Adapt<ChangeLogDto>());
return dtos;
}
public async Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>().Where(e => e.IdDiscriminator == idDiscriminator);
var datesCreateQuery = query
.Select(e => e.Creation)
.Distinct();
var datesCreate = await datesCreateQuery.ToArrayAsync(token);
var datesUpdateQuery = query
.Where(e => e.Obsolete != null)
.Select(e => e.Obsolete!.Value)
.Distinct();
var datesUpdate = await datesUpdateQuery.ToArrayAsync(token);
var dates = Enumerable.Concat(datesCreate, datesUpdate);
var datesOnly = dates
.Select(d => new DateOnly(d.Year, d.Month, d.Day))
.Distinct()
.OrderBy(d => d);
return datesOnly;
}
private ChangeLog CreateEntityFromDto(Guid idAuthor, Guid idDiscriminator, DataWithWellDepthAndSectionDto dto)
{
var entity = new ChangeLog()
{
Id = Uuid7.Guid(),
Creation = DateTimeOffset.UtcNow,
IdAuthor = idAuthor,
IdDiscriminator = idDiscriminator,
IdEditor = idAuthor,
Value = dto.Value,
IdSection = dto.IdSection,
DepthStart = dto.DepthStart,
DepthEnd = dto.DepthEnd,
};
return entity;
}
public async Task<IEnumerable<DataWithWellDepthAndSectionDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token)
{
var date = dateBegin.ToUniversalTime();
var query = this.db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.Where(e => e.Creation >= date || e.Obsolete >= date);
var entities = await query.ToArrayAsync(token);
var dtos = entities.Select(Convert);
return dtos;
}
public async Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token)
{
var query = db.Set<ChangeLog>()
.Where(e => e.IdDiscriminator == idDiscriminator)
.GroupBy(e => 1)
.Select(group => new
{
Min = group.Min(e => e.Creation),
Max = group.Max(e => (e.Obsolete.HasValue && e.Obsolete > e.Creation)
? e.Obsolete.Value
: e.Creation),
});
var values = await query.FirstOrDefaultAsync(token);
if (values is null)
{
return null;
}
return new DatesRangeDto
{
From = values.Min,
To = values.Max,
};
}
private DataWithWellDepthAndSectionDto Convert(ChangeLog entity) => entity.Adapt<DataWithWellDepthAndSectionDto>();
}

View File

@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Persistence.Database.Entity;
using Persistence.Models;
using Persistence.Models.Requests;
using Persistence.Repositories;
using Persistence.Repository.Extensions;
@ -24,7 +25,7 @@ namespace Persistence.Repository.Repositories
protected virtual IQueryable<TechMessage> GetQueryReadOnly() => db.Set<TechMessage>()
.Include(e => e.System);
public async Task<PaginationContainer<TechMessageDto>> GetPage(RequestDto request, CancellationToken token)
public async Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token)
{
var query = GetQueryReadOnly();
var count = await query.CountAsync(token);

View File

@ -1,45 +1,71 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.API;
/// <summary>
/// Интерфейс для работы с API журнала изменений
/// </summary>
public interface IChangeLogApi<TDto, TChangeLogDto>
where TDto : class, new()
where TChangeLogDto : ChangeLogDto<TDto>
public interface IChangeLogApi : ISyncWithDiscriminatorApi<DataWithWellDepthAndSectionDto>
{
/// <summary>
/// Получение исторических данных на текущую дату
/// Импорт с заменой: удаление старых строк и добавление новых
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<IEnumerable<TDto>>> GetChangeLogCurrent(CancellationToken token);
Task<IActionResult> ClearAndAddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение исторических данных на определенную дату
/// Получение данных на текущую дату (с пагинацией)
/// </summary>
/// <param name="historyMoment"></param>
/// <param name="idDiscriminator"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<IEnumerable<TChangeLogDto>>> GetChangeLogForDate(DateTimeOffset historyMoment, CancellationToken token);
Task<IActionResult> GetCurrent(Guid idDiscriminator, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение данных на определенную дату (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment"></param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение исторических данных за определенный период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetChangeLogForDate(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Добавить одну запись
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Add(TDto dto, CancellationToken token);
Task<IActionResult> Add(Guid idDiscriminator, DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Добавить несколько записей
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> AddRange(IEnumerable<TDto> dtos, CancellationToken token);
Task<IActionResult> AddRange(Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Обновить одну запись
@ -47,7 +73,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dto"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Update(TDto dto, CancellationToken token);
Task<IActionResult> Update(DataWithWellDepthAndSectionDto dto, CancellationToken token);
/// <summary>
/// Обновить несколько записей
@ -55,7 +81,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> UpdateRange(IEnumerable<TDto> dtos, CancellationToken token);
Task<IActionResult> UpdateRange(IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Удалить одну запись
@ -63,7 +89,7 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> Delete(int id, CancellationToken token);
Task<IActionResult> Delete(Guid id, CancellationToken token);
/// <summary>
/// Удалить несколько записей
@ -71,5 +97,13 @@ public interface IChangeLogApi<TDto, TChangeLogDto>
/// <param name="ids"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<int>> DeleteRange(IEnumerable<int> ids, CancellationToken token);
Task<IActionResult> DeleteRange(IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IActionResult> GetDatesChange(Guid idDiscriminator, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
namespace Persistence.API;

View File

@ -1,17 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Persistence.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Persistence.Models.Requests;
namespace Persistence.API;
/// Интерфейс для API, предназначенного для работы с табличными данными
public interface ITableDataApi<TDto, TRequest>
where TDto : class, new()
where TRequest : RequestDto
where TRequest : PaginationRequest
{
/// <summary>
/// Получить страницу списка объектов

114
Persistence/EFExtensions.cs Normal file
View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Persistence;
public static class EFExtensions
{
struct TypeAcessor
{
public LambdaExpression KeySelector { get; set; }
public MethodInfo OrderBy { get; set; }
public MethodInfo OrderByDescending { get; set; }
public MethodInfo ThenBy { get; set; }
public MethodInfo ThenByDescending { get; set; }
}
private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy");
private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending");
private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy");
private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending");
private static ConcurrentDictionary<Type, Dictionary<string, TypeAcessor>> TypePropSelectors { get; set; } = new();
private static MethodInfo GetExtOrderMethod(string methodName)
=> typeof(System.Linq.Queryable)
.GetMethods()
.Where(m => m.Name == methodName &&
m.IsGenericMethodDefinition &&
m.GetParameters().Length == 2 &&
m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression)))
.Single();
private static Dictionary<string, TypeAcessor> MakeTypeAcessors(Type type)
{
var propContainer = new Dictionary<string, TypeAcessor>();
var properties = type.GetProperties();
foreach (var propertyInfo in properties)
{
var name = propertyInfo.Name.ToLower();
ParameterExpression arg = Expression.Parameter(type, "x");
MemberExpression property = Expression.Property(arg, propertyInfo.Name);
var selector = Expression.Lambda(property, new ParameterExpression[] { arg });
var typeAccessor = new TypeAcessor
{
KeySelector = selector,
OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType),
OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType),
ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
};
propContainer.Add(name, typeAccessor);
}
return propContainer;
}
/// <summary>
/// Добавить в запрос сортировку по возрастанию или убыванию.
/// Этот метод сбросит ранее наложенные сортировки.
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <param name="query"></param>
/// <param name="propertySort">
/// Свойство сортировки.
/// Состоит из названия свойства (в любом регистре)
/// и опционально указания направления сортировки "asc" или "desc"
/// </param>
/// <example>
/// var query = query("Date desc");
/// </example>
/// <returns>Запрос с примененной сортировкой</returns>
public static IOrderedQueryable<TSource> SortBy<TSource>(
this IQueryable<TSource> query,
string propertySort)
{
var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries);
var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc";
var propertyName = parts[0];
var newQuery = query.SortBy(propertyName, isDesc);
return newQuery;
}
/// <summary>
/// Добавить в запрос сортировку по возрастанию или убыванию
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <param name="query"></param>
/// <param name="propertyName">Название свойства (в любом регистре)</param>
/// <param name="isDesc">Сортировать по убыванию</param>
/// <returns>Запрос с примененной сортировкой</returns>
public static IOrderedQueryable<TSource> SortBy<TSource>(
this IQueryable<TSource> query,
string propertyName,
bool isDesc)
{
var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAcessors);
var propertyNamelower = propertyName.ToLower();
var typeAccessor = typePropSelector[propertyNamelower];
var genericMethod = isDesc
? typeAccessor.OrderByDescending
: typeAccessor.OrderBy;
var newQuery = (IOrderedQueryable<TSource>)genericMethod
.Invoke(genericMethod, new object[] { query, typeAccessor.KeySelector })!;
return newQuery;
}
}

View File

@ -3,40 +3,40 @@ namespace Persistence.Models;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public class ChangeLogDto<T> where T: class
public class ChangeLogDto
{
/// <summary>
/// Запись
/// Ключ записи
/// </summary>
public required T Item { get; set; }
public Guid Id { get; set; }
/// <summary>
/// Автор
/// Создатель записи
/// </summary>
public UserDto? Author { get; set; }
public Guid IdAuthor { get; set; }
/// <summary>
/// Автор
/// Пользователь, изменивший запись
/// </summary>
public UserDto? Editor { get; set; }
public Guid? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// Дата создания
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например, при удалении)
/// Дата устаревания
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// Id состояния
/// Ключ заменившей записи
/// </summary>
public int IdState { get; set; }
public Guid? IdNext { get; set; }
/// <summary>
/// Id заменяемой записи
/// Объект записи
/// </summary>
public int? IdPrevious { get; set; }
public DataWithWellDepthAndSectionDto Value { get; set; } = default!;
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models;
/// <summary>
/// Dto для хранения записей, содержащих начальную и конечную глубину забоя, а также секцию
/// </summary>
public class DataWithWellDepthAndSectionDto
{
/// <summary>
/// Ключ записи
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid IdSection { get; set; }
/// <summary>
/// Объект записи
/// </summary>
public required IDictionary<string, object> Value { get; set; }
}

View File

@ -1,62 +0,0 @@
namespace Persistence.Models;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public interface IChangeLogAbstract
{
/// <summary>
/// Актуальная
/// </summary>
public const int IdStateActual = 0;
/// <summary>
/// Замененная
/// </summary>
public const int IdStateReplaced = 1;
/// <summary>
/// Удаленная
/// </summary>
public const int IdStateDeleted = 2;
/// <summary>
/// Очищено при импорте
/// </summary>
public const int IdCleared = 3;
/// <summary>
/// Ид записи
/// </summary>
public int Id { get; set; }
/// <summary>
/// Автор изменения
/// </summary>
public int IdAuthor { get; set; }
/// <summary>
/// Редактор
/// </summary>
public int? IdEditor { get; set; }
/// <summary>
/// Дата создания записи
/// </summary>
public DateTimeOffset Creation { get; set; }
/// <summary>
/// Дата устаревания (например при удалении)
/// </summary>
public DateTimeOffset? Obsolete { get; set; }
/// <summary>
/// "ИД состояния записи: \n0 - актуальная\n1 - замененная\n2 - удаленная
/// </summary>
public int IdState { get; set; }
/// <summary>
/// Id заменяемой записи
/// </summary>
public int? IdPrevious { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Persistence.Models;
public interface IWithSectionPart
{
public double DepthStart { get; set; }
public double DepthEnd { get; set; }
public Guid IdSection { get; set; }
}

View File

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

View File

@ -0,0 +1,22 @@
namespace Persistence.Models.Requests;
/// <summary>
/// Запрос для фильтрации данных по секции и глубине
/// </summary>
public class SectionPartRequest
{
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
public double? DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
public double? DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
public Guid? IdSection { get; set; }
}

View File

@ -1,165 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
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;
// 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();
// //var updateTime = DateTimeOffset.UtcNow;
// ////todo
// //var query = BuildQuery(request);
// //query = query.Where(e => e.Obsolete == null);
// //var entitiesToDelete = await query.ToArrayAsync(token);
// //foreach (var entity in entitiesToDelete)
// //{
// // entity.IdState = IChangeLogAbstract.IdCleared;
// // entity.Obsolete = updateTime;
// // entity.IdEditor = idUser;
// //}
// //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);
// 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<DateOnly>> GetDatesChange(CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset date, CancellationToken token)
// {
// throw new NotImplementedException();
// }
// public async Task<int> AddRange(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();
// 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();
// }
// 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);
// }
// result += await SaveChangesWithExceptionHandling(token);
// }
// 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 static void TryConvertPostgresExceptionToValidateException(PostgresException pgException)
// //{
// // if (pgException.SqlState == PostgresErrorCodes.ForeignKeyViolation)
// // throw new ArgumentInvalidException("dtos", pgException.Message + "\r\n" + pgException.Detail);
// //}
//}

View File

@ -1,4 +1,5 @@
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repositories;
@ -6,84 +7,81 @@ namespace Persistence.Repositories;
/// Интерфейс для работы с историческими данными
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface IChangeLogRepository<TDto, TChangeLogDto> : ISyncRepository<TDto>
where TDto : class, ITimeSeriesAbstractDto, new()
where TChangeLogDto : ChangeLogDto<TDto>
public interface IChangeLogRepository : ISyncWithDiscriminatorRepository<DataWithWellDepthAndSectionDto>
{
/// <summary>
/// Добавление записей
/// </summary>
/// <param name="idUser">пользователь, который добавляет</param>
/// <param name="idAuthor">пользователь, который добавляет</param>
/// <param name="idDiscriminator">ключ справочника</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Редактирование записей
/// </summary>
/// <param name="idUser">пользователь, который редактирует</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Добавляет Dto у которых id == 0, изменяет dto у которых id != 0
/// </summary>
/// <param name="idUser">пользователь, который редактирует или добавляет</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
/// <summary>
/// Помечает записи как удаленные
/// </summary>
/// <param name="idUser">пользователь, который чистит</param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> Clear(int idUser, CancellationToken token);
/// <summary>
/// Очистить и добавить новые
/// </summary>
/// <param name="idUser"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token);
Task<int> AddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Пометить записи как удаленные
/// </summary>
/// <param name="idUser"></param>
/// <param name="ids"></param>
/// <param name="idEditor"></param>
/// <param name="ids">ключи записей</param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token);
Task<int> MarkAsDeleted(Guid idEditor, IEnumerable<Guid> ids, CancellationToken token);
/// <summary>
/// Получение дат изменений записей
/// Пометить записи как удаленные
/// </summary>
/// <param name="request"></param>
/// <param name="idEditor"></param>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token);
Task<int> MarkAsDeleted(Guid idEditor, Guid idDiscriminator, CancellationToken token);
/// <summary>
/// Получение измененных записей за определенную дату
/// Очистить и добавить новые
/// </summary>
/// <param name="updateFrom"></param>
/// <param name="idAuthor"></param>
/// <param name="idDiscriminator"></param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token);
Task<int> ClearAndAddRange(Guid idAuthor, Guid idDiscriminator, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение текущих сейчас записей по параметрам
/// Редактирование записей
/// </summary>
/// <param name="request"></param>
/// <param name="idEditor">пользователь, который редактирует</param>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token);
Task<int> UpdateRange(Guid idEditor, IEnumerable<DataWithWellDepthAndSectionDto> dtos, CancellationToken token);
/// <summary>
/// Получение актуальных записей на определенный момент времени (с пагинацией)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="moment">текущий момент времени</param>
/// <param name="filterRequest">параметры запроса фильтрации</param>
/// <param name="paginationRequest">параметры запроса пагинации</param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<DataWithWellDepthAndSectionDto>> GetByDate(Guid idDiscriminator, DateTimeOffset moment, SectionPartRequest filterRequest, PaginationRequest paginationRequest, CancellationToken token);
/// <summary>
/// Получение измененных записей за период времени
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="dateBegin"></param>
/// <param name="dateEnd"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<ChangeLogDto>> GetChangeLogForInterval(Guid idDiscriminator, DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token);
/// <summary>
/// Получение списка дат, в которые происходили изменения (день, месяц, год, без времени)
/// </summary>
/// <param name="idDiscriminator"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<DateOnly>> GetDatesChange(Guid idDiscriminator, CancellationToken token);
}

View File

@ -1,17 +1,25 @@
namespace Persistence.Repositories;
using Persistence.Models;
namespace Persistence.Repositories;
/// <summary>
/// Интерфейс по работе с данными
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface ISyncRepository<TDto>
where TDto : class, new()
{
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="dateBegin">дата начала</param>
/// <param name="token"></param> /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token);
Task<DatesRangeDto?> GetDatesRange(CancellationToken token);
}

View File

@ -0,0 +1,28 @@
using Persistence.Models;
namespace Persistence.Repositories;
/// <summary>
/// Интерфейс по работе с данными, у которых есть дискриминатор
/// </summary>
/// <typeparam name="TDto"></typeparam>
public interface ISyncWithDiscriminatorRepository<TDto>
{
/// <summary>
/// Получить данные, начиная с определенной даты
/// </summary>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="dateBegin">дата начала</param>
/// <param name="token"></param>
/// /// <returns></returns>
Task<IEnumerable<TDto>> GetGtDate(Guid idDiscriminator, DateTimeOffset dateBegin, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="idDiscriminator">дискриминатор таблицы</param>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(Guid idDiscriminator, CancellationToken token);
}

View File

@ -1,4 +1,4 @@
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repositories;
@ -7,7 +7,7 @@ namespace Persistence.Repositories;
/// </summary>
public interface ITableDataRepository<TDto, TRequest>
where TDto : class, new()
where TRequest : RequestDto
where TRequest : PaginationRequest
{
/// <summary>
/// Получить страницу списка объектов

View File

@ -1,59 +1,59 @@
using System.Threading.Tasks;
using Persistence.Models;
using Persistence.Models;
using Persistence.Models.Requests;
namespace Persistence.Repositories
{
/// <summary>
/// Интерфейс по работе с технологическими сообщениями
/// </summary>
public interface ITechMessagesRepository
{
/// <summary>
/// Получить страницу списка объектов
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<TechMessageDto>> GetPage(RequestDto request, CancellationToken token);
/// <summary>
/// Интерфейс по работе с технологическими сообщениями
/// </summary>
public interface ITechMessagesRepository
{
/// <summary>
/// Получить страницу списка объектов
/// </summary>
/// <param name="request"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<PaginationContainer<TechMessageDto>> GetPage(PaginationRequest request, CancellationToken token);
/// <summary>
/// Добавление новых сообщений
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token);
/// <summary>
/// Добавление новых сообщений
/// </summary>
/// <param name="dtos"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> AddRange(IEnumerable<TechMessageDto> dtos, Guid userId, CancellationToken token);
/// <summary>
/// Получение списка уникальных названий систем АБ
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<string>> GetSystems(CancellationToken token);
/// <summary>
/// Получение списка уникальных названий систем АБ
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<string>> GetSystems(CancellationToken token);
/// <summary>
/// Получение количества сообщений по категориям и системам автобурения
/// </summary>
/// <param name="categoryId">Id Категории важности</param>
/// <param name="autoDrillingSystem">Система автобурения</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token);
/// <summary>
/// Получение количества сообщений по категориям и системам автобурения
/// </summary>
/// <param name="categoryId">Id Категории важности</param>
/// <param name="autoDrillingSystem">Система автобурения</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<MessagesStatisticDto>> GetStatistics(IEnumerable<string> autoDrillingSystem, IEnumerable<int> categoryIds, CancellationToken token);
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary>
/// Получить порцию записей, начиная с заданной даты
/// </summary>
/// <param name="dateBegin"></param>
/// <param name="take"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TechMessageDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
}
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
}
}

View File

@ -19,11 +19,4 @@ public interface ITimeSeriesBaseRepository<TDto>
double intervalSec = 600d,
int approxPointsCount = 1024,
CancellationToken token = default);
/// <summary>
/// Получить диапазон дат, для которых есть данные в репозитории
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
Task<DatesRangeDto?> GetDatesRange(CancellationToken token);
}