Merge branch 'master' into TimestampedValuesRepository
All checks were successful
Unit tests / test (push) Successful in 2m44s
All checks were successful
Unit tests / test (push) Successful in 2m44s
This commit is contained in:
commit
0c1210fbcc
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using DD.Persistence.Models;
|
||||
using DD.Persistence.Repositories;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using DD.Persistence.Models.Common;
|
||||
|
||||
namespace DD.Persistence.API.Controllers;
|
||||
@ -29,9 +30,9 @@ public class SetpointController : ControllerBase, ISetpointApi
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("current")]
|
||||
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||
public async Task<ActionResult<Dictionary<Guid, object>>> GetCurrent([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||
{
|
||||
var result = await setpointRepository.GetCurrent(setpointKeys, token);
|
||||
var result = await setpointRepository.GetCurrentDictionary(setpointKeys, token);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
@ -105,7 +106,7 @@ public class SetpointController : ControllerBase, ISetpointApi
|
||||
public async Task<IActionResult> Add(Guid setpointKey, object newValue, CancellationToken token)
|
||||
{
|
||||
var userId = User.GetUserId<Guid>();
|
||||
await setpointRepository.Add(setpointKey, newValue, userId, token);
|
||||
await setpointRepository.Add(setpointKey, (JsonElement)newValue, userId, token);
|
||||
|
||||
return CreatedAtAction(nameof(Add), true);
|
||||
}
|
||||
|
51
DD.Persistence.API/Readme.md
Normal file
51
DD.Persistence.API/Readme.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Persistence Service Readme
|
||||
|
||||
## Краткое описание
|
||||
Persistence сервис отвечает за работу с хранимыми данными
|
||||
в рамках совокупности различных систем.
|
||||
|
||||
## Локальное развертывание
|
||||
1. Скачать репозиторий по SSH
|
||||
```
|
||||
ssh://git@git.ddrilling.ru:2221/on.nemtina/persistence.git
|
||||
```
|
||||
|
||||
Для доступа к репозиториям редварительно необходимо сгенерировать SSH ключ и добавить его в Gitea
|
||||
|
||||
2. Выбрать ветку dev
|
||||
|
||||
## Использование Swagger-а
|
||||
1. Сконфигурировать appsettings.Development.json
|
||||
(при отсутствии) занести флаг:
|
||||
```json
|
||||
"NeedUseKeyCloak": true
|
||||
```
|
||||
2. Запустить решение в режиме Debug
|
||||
3. Выполнить авторизацию через KeyCloak - качестве client_id указать:
|
||||
```
|
||||
webapi
|
||||
```
|
||||
После этого должен произойти редирект на страницу авторизации в KeyCloak
|
||||
|
||||
4. Заполнить поля и авторизоваться
|
||||
```
|
||||
Username or email: myuser
|
||||
```
|
||||
```
|
||||
Password: 12345
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
Запуск тестов рекомендуется осуществлять без использования KeyCloak<br> Для этого
|
||||
настройка appsettings.Tests.json должна содержать:
|
||||
```
|
||||
"NeedUseKeyCloak": false,
|
||||
"AuthUser": {
|
||||
"username": "myuser",
|
||||
"password": 12345,
|
||||
"clientId": "webapi",
|
||||
"grantType": "password"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -25,12 +25,21 @@ public interface ISetpointClient : IDisposable
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Получить диапазон дат, для которых есть данные в репозитории
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
|
||||
/// <summary>
|
||||
/// Получить актуальные значения уставок
|
||||
/// </summary>
|
||||
/// <param name="setpointConfigs"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>s
|
||||
Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointConfigs, CancellationToken token);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Получить диапазон дат, для которых есть данные в репозитории
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<DatesRangeDto> GetDatesRangeAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Получить значения уставок за определенный момент времени
|
||||
|
@ -1,6 +1,7 @@
|
||||
using DD.Persistence.Models;
|
||||
using DD.Persistence.Models.Common;
|
||||
using Refit;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DD.Persistence.Client.Clients.Interfaces.Refit;
|
||||
|
||||
@ -8,8 +9,11 @@ public interface IRefitSetpointClient : IRefitClient, IDisposable
|
||||
{
|
||||
private const string BaseRoute = "/api/setpoint";
|
||||
|
||||
//[Get($"{BaseRoute}/current")]
|
||||
//Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
|
||||
[Get($"{BaseRoute}/current")]
|
||||
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
Task<IApiResponse<Dictionary<Guid, JsonElement>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
|
||||
[Get($"{BaseRoute}/history")]
|
||||
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, [Query] DateTimeOffset historyMoment, CancellationToken token);
|
||||
|
@ -1,8 +1,11 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DD.Persistence.Client.Clients.Base;
|
||||
using DD.Persistence.Client.Clients.Interfaces;
|
||||
using DD.Persistence.Client.Clients.Interfaces.Refit;
|
||||
using DD.Persistence.Models;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Globalization;
|
||||
using DD.Persistence.Models.Common;
|
||||
|
||||
namespace DD.Persistence.Client.Clients;
|
||||
@ -10,25 +13,48 @@ namespace DD.Persistence.Client.Clients;
|
||||
public class SetpointClient : BaseClient, ISetpointClient
|
||||
{
|
||||
private readonly IRefitSetpointClient refitSetpointClient;
|
||||
private readonly ISetpointConfigStorage setpointConfigStorage;
|
||||
|
||||
public SetpointClient(IRefitClientFactory<IRefitSetpointClient> refitSetpointClientFactory, ILogger<SetpointClient> logger) : base(logger)
|
||||
public SetpointClient(
|
||||
IRefitClientFactory<IRefitSetpointClient> refitSetpointClientFactory,
|
||||
ISetpointConfigStorage setpointConfigStorage,
|
||||
ILogger<SetpointClient> logger) : base(logger)
|
||||
{
|
||||
this.refitSetpointClient = refitSetpointClientFactory.Create();
|
||||
}
|
||||
this.setpointConfigStorage = setpointConfigStorage;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||
{
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetCurrent(setpointKeys, token), token);
|
||||
|
||||
return result!;
|
||||
return result!.Select(x => new SetpointValueDto {
|
||||
Key = x.Key,
|
||||
Value = DeserializeValue(x.Key, x.Value)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
|
||||
|
||||
|
||||
public async Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointConfigs, CancellationToken token)
|
||||
{
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetCurrent(setpointConfigs, token), token);
|
||||
|
||||
|
||||
return result!.ToDictionary(x => x.Key,x => DeserializeValue(x.Key,x.Value));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
|
||||
{
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetHistory(setpointKeys, historyMoment, token), token);
|
||||
|
||||
foreach(var dto in result)
|
||||
dto.Value = DeserializeValue(dto.Key, (JsonElement)dto.Value);
|
||||
|
||||
|
||||
return result!;
|
||||
}
|
||||
|
||||
@ -37,6 +63,9 @@ public class SetpointClient : BaseClient, ISetpointClient
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetLog(setpointKeys, token), token);
|
||||
|
||||
foreach(var item in result)
|
||||
DeserializeList(result[item.Key]);
|
||||
|
||||
return result!;
|
||||
}
|
||||
|
||||
@ -49,14 +78,18 @@ public class SetpointClient : BaseClient, ISetpointClient
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SetpointLogDto>> GetPart(DateTimeOffset dateBegin, int take, CancellationToken token)
|
||||
{
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetPart(dateBegin, take, token), token);
|
||||
{
|
||||
var result = await ExecuteGetResponse(
|
||||
async () => await refitSetpointClient.GetPart(dateBegin, take, token), token);
|
||||
|
||||
return result!;
|
||||
}
|
||||
DeserializeList(result);
|
||||
|
||||
public async Task Add(Guid setpointKey, object newValue, CancellationToken token)
|
||||
return result!;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task Add(Guid setpointKey, object newValue, CancellationToken token)
|
||||
{
|
||||
await ExecutePostResponse(
|
||||
async () => await refitSetpointClient.Add(setpointKey, newValue, token), token);
|
||||
@ -68,4 +101,21 @@ public class SetpointClient : BaseClient, ISetpointClient
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
private object DeserializeValue(Guid key, JsonElement value)
|
||||
{
|
||||
if (setpointConfigStorage.TryGetType(key, out var type))
|
||||
return value.Deserialize(type)!;
|
||||
|
||||
return value;
|
||||
}
|
||||
private void DeserializeList(IEnumerable<SetpointLogDto>? result)
|
||||
{
|
||||
foreach (var log in result)
|
||||
log.Value = DeserializeValue(log.Key, (JsonElement)log.Value);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,9 +11,9 @@
|
||||
<!--Наименование-->
|
||||
<Title>DD.Persistence.Client</Title>
|
||||
<!--Версия пакета-->
|
||||
<VersionPrefix>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
|
||||
<VersionPrefix>1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</VersionPrefix>
|
||||
<!--Версия сборки-->
|
||||
<AssemblyVersion>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
|
||||
<AssemblyVersion>1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1</AssemblyVersion>
|
||||
<!--Id пакета-->
|
||||
<PackageId>DD.Persistence.Client</PackageId>
|
||||
|
||||
@ -33,15 +33,15 @@
|
||||
<!--Формат пакета с символами-->
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<!--Путь к пакету-->
|
||||
<PackageOutputPath>C:\Projects\Nuget\Persistence\Client</PackageOutputPath>
|
||||
<PackageOutputPath>C:\Projects\Nuget\Persistence</PackageOutputPath>
|
||||
|
||||
<!--Readme-->
|
||||
<PackageReadmeFile>Readme.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
|
||||
<AssemblyVersion>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
|
||||
<VersionPrefix>1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
|
||||
<AssemblyVersion>1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -15,7 +15,7 @@ public static class DependencyInjection
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddPersistenceClients(this IServiceCollection services)
|
||||
public static IServiceCollection AddPersistenceClients(this IServiceCollection services, Dictionary<Guid, Type>? setpointTypeConfigs = null)
|
||||
{
|
||||
services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>));
|
||||
services.AddTransient<IChangeLogClient, ChangeLogClient>();
|
||||
@ -24,6 +24,11 @@ public static class DependencyInjection
|
||||
services.AddTransient<ITechMessagesClient, TechMessagesClient>();
|
||||
services.AddTransient<ITimestampedValuesClient, TimestampedValuesClient>();
|
||||
services.AddTransient<IWitsDataClient, WitsDataClient>();
|
||||
|
||||
services.AddSingleton<ISetpointConfigStorage, SetpointConfigStorage>(provider =>
|
||||
{
|
||||
return new SetpointConfigStorage(setpointTypeConfigs);
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
11
DD.Persistence.Client/ISetpointConfigStorage.cs
Normal file
11
DD.Persistence.Client/ISetpointConfigStorage.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DD.Persistence.Client;
|
||||
public interface ISetpointConfigStorage
|
||||
{
|
||||
bool TryGetType(Guid id, out Type type);
|
||||
}
|
20
DD.Persistence.Client/SetpointConfigStorage.cs
Normal file
20
DD.Persistence.Client/SetpointConfigStorage.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace DD.Persistence.Client;
|
||||
internal class SetpointConfigStorage : ISetpointConfigStorage
|
||||
{
|
||||
private readonly Dictionary<Guid, Type> setpointTypeConfigs;
|
||||
|
||||
public SetpointConfigStorage(Dictionary<Guid, Type>? setpointTypeConfigs)
|
||||
{
|
||||
this.setpointTypeConfigs = setpointTypeConfigs?? new Dictionary<Guid, Type>();
|
||||
}
|
||||
|
||||
public bool TryGetType(Guid id, out Type type)
|
||||
{
|
||||
return setpointTypeConfigs.TryGetValue(id, out type);
|
||||
}
|
||||
|
||||
public void AddOrReplace(Guid id, Type type)
|
||||
{
|
||||
setpointTypeConfigs[id] = type;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using DD.Persistence.Database.EntityAbstractions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DD.Persistence.Database.Model
|
||||
{
|
||||
@ -11,7 +12,7 @@ namespace DD.Persistence.Database.Model
|
||||
public Guid Key { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb"), Comment("Значение уставки")]
|
||||
public required object Value { get; set; }
|
||||
public required JsonElement Value { get; set; }
|
||||
|
||||
[Comment("Дата создания уставки")]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
@ -5,6 +5,7 @@ using DD.Persistence.Client.Clients.Interfaces.Refit;
|
||||
using DD.Persistence.Database.Model;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace DD.Persistence.IntegrationTests.Controllers
|
||||
@ -12,6 +13,7 @@ namespace DD.Persistence.IntegrationTests.Controllers
|
||||
public class SetpointControllerTest : BaseIntegrationTest
|
||||
{
|
||||
private readonly ISetpointClient setpointClient;
|
||||
private readonly SetpointConfigStorage configStorage;
|
||||
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
|
||||
{
|
||||
var refitClientFactory = scope.ServiceProvider
|
||||
@ -20,8 +22,36 @@ namespace DD.Persistence.IntegrationTests.Controllers
|
||||
|
||||
setpointClient = scope.ServiceProvider
|
||||
.GetRequiredService<ISetpointClient>();
|
||||
|
||||
configStorage = (SetpointConfigStorage)scope.ServiceProvider.GetRequiredService<ISetpointConfigStorage>();
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetCurrent_returns_correctType()
|
||||
{
|
||||
var id = Guid.Parse("e0fcad22-1761-476e-a729-a3c59d51ba41");
|
||||
|
||||
configStorage.AddOrReplace(id, typeof(float));
|
||||
|
||||
await setpointClient.Add(id, 48.3f, CancellationToken.None);
|
||||
|
||||
//act
|
||||
var response = await setpointClient.GetCurrent([id], CancellationToken.None);
|
||||
|
||||
//assert
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response);
|
||||
Assert.Single(response);
|
||||
var item = response.First();
|
||||
Assert.Equal(item.Key, id);
|
||||
|
||||
Assert.IsNotType<JsonElement>(item.Value);
|
||||
Assert.Equal(item.Value, 48.3f);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetCurrent_returns_success()
|
||||
{
|
||||
@ -33,7 +63,7 @@ namespace DD.Persistence.IntegrationTests.Controllers
|
||||
};
|
||||
|
||||
//act
|
||||
var response = await setpointClient.GetCurrent(setpointKeys, new CancellationToken());
|
||||
var response = await setpointClient.GetCurrent(setpointKeys, CancellationToken.None);
|
||||
|
||||
//assert
|
||||
Assert.NotNull(response);
|
||||
|
@ -11,9 +11,9 @@
|
||||
<!--Наименование-->
|
||||
<Title>DD.Persistence.Models</Title>
|
||||
<!--Версия пакета-->
|
||||
<VersionPrefix>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
|
||||
<VersionPrefix>1.2.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</VersionPrefix>
|
||||
<!--Версия сборки-->
|
||||
<AssemblyVersion>1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
|
||||
<AssemblyVersion>1.2.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH))</AssemblyVersion>
|
||||
<!--Id пакета-->
|
||||
<PackageId>DD.Persistence.Models</PackageId>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<!--Формат пакета с символами-->
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<!--Путь к пакету-->
|
||||
<PackageOutputPath>C:\Projects\Nuget\Persistence\Models</PackageOutputPath>
|
||||
<PackageOutputPath>C:\Projects\Nuget\Persistence</PackageOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||
<PackageReference Include="Testcontainers" Version="4.1.0" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.1.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DD.Persistence.Database.Postgres\DD.Persistence.Database.Postgres.csproj" />
|
||||
<ProjectReference Include="..\DD.Persistence.Repository\DD.Persistence.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
32
DD.Persistence.Repository.Test/RepositoryTestFixture.cs
Normal file
32
DD.Persistence.Repository.Test/RepositoryTestFixture.cs
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
using DD.Persistence.Database;
|
||||
using DD.Persistence.Database.Model;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
namespace DD.Persistence.Repository.Test;
|
||||
|
||||
public class RepositoryTestFixture : IAsyncLifetime
|
||||
{
|
||||
public readonly PostgreSqlContainer dbContainer = new PostgreSqlBuilder().Build();
|
||||
|
||||
|
||||
public PersistencePostgresContext GetDbContext() => new(new DbContextOptionsBuilder<PersistencePostgresContext>()
|
||||
.UseNpgsql(dbContainer.GetConnectionString()).Options);
|
||||
|
||||
public IMemoryCache GetMemoryCache() => new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
public virtual async Task InitializeAsync()
|
||||
{
|
||||
await dbContainer.StartAsync();
|
||||
var forumDbContext = new PersistencePostgresContext(new DbContextOptionsBuilder<PersistencePostgresContext>()
|
||||
.UseNpgsql(dbContainer.GetConnectionString()).Options);
|
||||
|
||||
await forumDbContext.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync() => await dbContainer.DisposeAsync();
|
||||
}
|
||||
|
56
DD.Persistence.Repository.Test/SetpointRepositoryShould.cs
Normal file
56
DD.Persistence.Repository.Test/SetpointRepositoryShould.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using DD.Persistence.Database.Model;
|
||||
using DD.Persistence.Repository.Repositories;
|
||||
using Shouldly;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DD.Persistence.Repository.Test;
|
||||
public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
|
||||
{
|
||||
private readonly RepositoryTestFixture fixture;
|
||||
private readonly PersistencePostgresContext context;
|
||||
private readonly SetpointRepository sut;
|
||||
|
||||
public SetpointRepositoryShould(RepositoryTestFixture fixture)
|
||||
{
|
||||
this.fixture = fixture;
|
||||
context = fixture.GetDbContext();
|
||||
sut = new SetpointRepository(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnValueKindNumber()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var value = GetJsonFromObject(22);
|
||||
await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
var t = fixture.dbContainer.GetConnectionString();
|
||||
//act
|
||||
var result = await sut.GetCurrent([id], CancellationToken.None);
|
||||
|
||||
|
||||
//assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldNotBeEmpty();
|
||||
|
||||
var setpoint = result.First();
|
||||
|
||||
setpoint.Value.ShouldNotBeNull();
|
||||
setpoint
|
||||
.Value.ShouldBeOfType<JsonElement>()
|
||||
.ValueKind.ShouldBe(JsonValueKind.Number);
|
||||
}
|
||||
|
||||
private JsonElement GetJsonFromObject(object value)
|
||||
{
|
||||
var jsonString = JsonSerializer.Serialize(value);
|
||||
var doc = JsonDocument.Parse(jsonString);
|
||||
return doc.RootElement;
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ namespace DD.Persistence.Repository.Extensions;
|
||||
|
||||
public static class EFExtensionsSortBy
|
||||
{
|
||||
struct TypeAccessor
|
||||
public struct TypeAccessor
|
||||
{
|
||||
public LambdaExpression KeySelector { get; set; }
|
||||
public MethodInfo OrderBy { get; set; }
|
||||
@ -26,6 +26,42 @@ public static class EFExtensionsSortBy
|
||||
|
||||
private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending");
|
||||
|
||||
public static Func<Type, Type?, TypeAccessor?, Tuple<MethodInfo, MethodInfo>> sortOrder =
|
||||
(Type rootType, Type? type, TypeAccessor? accessor) =>
|
||||
{
|
||||
if (type is null && accessor.HasValue)
|
||||
{
|
||||
var accessorValue = accessor.Value;
|
||||
return Tuple.Create(
|
||||
accessorValue.OrderBy,
|
||||
accessorValue.OrderByDescending
|
||||
);
|
||||
}
|
||||
|
||||
return Tuple.Create(
|
||||
methodOrderBy.MakeGenericMethod(rootType, type!),
|
||||
methodOrderByDescending.MakeGenericMethod(rootType, type!)
|
||||
);
|
||||
};
|
||||
|
||||
public static Func<Type, Type?, TypeAccessor?, Tuple<MethodInfo, MethodInfo>> thenSortOrder =
|
||||
(Type rootType, Type? type, TypeAccessor? accessor) =>
|
||||
{
|
||||
if (type is null && accessor.HasValue)
|
||||
{
|
||||
var accessorValue = accessor.Value;
|
||||
return Tuple.Create(
|
||||
accessorValue.ThenBy,
|
||||
accessorValue.ThenByDescending
|
||||
);
|
||||
}
|
||||
|
||||
return Tuple.Create(
|
||||
methodThenBy.MakeGenericMethod(rootType, type!),
|
||||
methodThenByDescending.MakeGenericMethod(rootType, type!)
|
||||
);
|
||||
};
|
||||
|
||||
private static MethodInfo GetExtOrderMethod(string methodName)
|
||||
=> typeof(Queryable)
|
||||
.GetMethods()
|
||||
@ -71,7 +107,7 @@ public static class EFExtensionsSortBy
|
||||
/// и опционально указания направления сортировки "asc" или "desc"
|
||||
/// </param>
|
||||
/// <example>
|
||||
/// var query = query("Timestamp desc");
|
||||
/// var query = query("Date desc");
|
||||
/// </example>
|
||||
/// <returns>Запрос с примененной сортировкой</returns>
|
||||
public static IOrderedQueryable<TSource> SortBy<TSource>(
|
||||
@ -83,10 +119,11 @@ public static class EFExtensionsSortBy
|
||||
|
||||
var sortEnum = propertySorts.GetEnumerator();
|
||||
sortEnum.MoveNext();
|
||||
var orderedQuery = query.SortBy(sortEnum.Current);
|
||||
|
||||
var orderedQuery = query.SortBy(sortOrder, sortEnum.Current);
|
||||
|
||||
while (sortEnum.MoveNext())
|
||||
orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current);
|
||||
orderedQuery = orderedQuery.SortBy(thenSortOrder, sortEnum.Current);
|
||||
|
||||
return orderedQuery;
|
||||
}
|
||||
@ -103,44 +140,19 @@ public static class EFExtensionsSortBy
|
||||
/// и опционально указания направления сортировки "asc" или "desc"
|
||||
/// </param>
|
||||
/// <example>
|
||||
/// var query = query("Timestamp desc");
|
||||
/// var query = query("Date desc");
|
||||
/// </example>
|
||||
/// <returns>Запрос с примененной сортировкой</returns>
|
||||
public static IOrderedQueryable<TSource> SortBy<TSource>(
|
||||
this IQueryable<TSource> query,
|
||||
Func<Type, Type?, TypeAccessor?, Tuple<MethodInfo, MethodInfo>> orderMethod,
|
||||
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="propertySort">
|
||||
/// Свойство сортировки.
|
||||
/// Состоит из названия свойства (в любом регистре)
|
||||
/// и опционально указания направления сортировки "asc" или "desc"
|
||||
/// </param>
|
||||
/// <example>
|
||||
/// var query = query("Timestamp desc");
|
||||
/// </example>
|
||||
/// <returns>Запрос с примененной сортировкой</returns>
|
||||
public static IOrderedQueryable<TSource> ThenSortBy<TSource>(
|
||||
this IOrderedQueryable<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.ThenSortBy(propertyName, isDesc);
|
||||
var newQuery = query.SortBy(orderMethod, propertyName, isDesc);
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
@ -154,25 +166,27 @@ public static class EFExtensionsSortBy
|
||||
/// <returns>Запрос с примененной сортировкой</returns>
|
||||
public static IOrderedQueryable<TSource> SortBy<TSource>(
|
||||
this IQueryable<TSource> query,
|
||||
Func<Type, Type?, TypeAccessor?, Tuple<MethodInfo, MethodInfo>> orderMethod,
|
||||
string propertyName,
|
||||
bool isDesc)
|
||||
{
|
||||
Type rootType = typeof(TSource);
|
||||
var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors);
|
||||
var propertyNameLower = propertyName.ToLower();
|
||||
|
||||
MethodInfo orderByDescending;
|
||||
MethodInfo orderByAscending;
|
||||
|
||||
TypeAccessor? rootTypeAccessor = null;
|
||||
Type? type = null;
|
||||
LambdaExpression? lambdaExpression = null;
|
||||
|
||||
if (propertyName.Contains('.'))
|
||||
const string Separator = ".";
|
||||
if (propertyName.Contains(Separator))
|
||||
{
|
||||
Type type = rootType;
|
||||
type = rootType;
|
||||
ParameterExpression rootExpression = Expression.Parameter(rootType, "x");
|
||||
Expression expr = rootExpression;
|
||||
|
||||
var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries);
|
||||
var propertyPath = propertyName.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
for (int i = 0; i < propertyPath.Length; i++)
|
||||
{
|
||||
@ -184,75 +198,24 @@ public static class EFExtensionsSortBy
|
||||
Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type);
|
||||
lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression);
|
||||
|
||||
orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type);
|
||||
orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type);
|
||||
Tuple<MethodInfo, MethodInfo> order = orderMethod
|
||||
.Invoke(rootType, type, null);
|
||||
orderByAscending = order.Item1;
|
||||
orderByDescending = order.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootTypeAccessor = typePropSelector[propertyNameLower];
|
||||
orderByAscending = rootTypeAccessor.OrderBy;
|
||||
orderByDescending = rootTypeAccessor.OrderByDescending;
|
||||
lambdaExpression = rootTypeAccessor.KeySelector;
|
||||
}
|
||||
var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors);
|
||||
var propertyNameLower = propertyName.ToLower();
|
||||
|
||||
var genericMethod = isDesc
|
||||
? orderByDescending
|
||||
: orderByAscending;
|
||||
rootTypeAccessor = typePropSelector[propertyNameLower];
|
||||
|
||||
var newQuery = (IOrderedQueryable<TSource>)genericMethod
|
||||
.Invoke(genericMethod, [query, lambdaExpression])!;
|
||||
return newQuery;
|
||||
}
|
||||
Tuple<MethodInfo, MethodInfo> order = orderMethod
|
||||
.Invoke(rootType, type, rootTypeAccessor);
|
||||
orderByAscending = order.Item1;
|
||||
orderByDescending = order.Item2;
|
||||
|
||||
/// <summary>
|
||||
/// Добавить в запрос дополнительную сортировку по возрастанию или убыванию
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource"></typeparam>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="propertyName">Название свойства (в любом регистре)</param>
|
||||
/// <param name="isDesc">Сортировать по убыванию</param>
|
||||
/// <returns>Запрос с примененной сортировкой</returns>
|
||||
public static IOrderedQueryable<TSource> ThenSortBy<TSource>(
|
||||
this IOrderedQueryable<TSource> query,
|
||||
string propertyName,
|
||||
bool isDesc)
|
||||
{
|
||||
Type rootType = typeof(TSource);
|
||||
var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors);
|
||||
var propertyNameLower = propertyName.ToLower();
|
||||
|
||||
MethodInfo orderByDescending;
|
||||
MethodInfo orderByAscending;
|
||||
|
||||
LambdaExpression? lambdaExpression = null;
|
||||
|
||||
if (propertyName.Contains('.'))
|
||||
{
|
||||
Type type = rootType;
|
||||
ParameterExpression rootExpression = Expression.Parameter(rootType, "x");
|
||||
Expression expr = rootExpression;
|
||||
|
||||
var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
for (int i = 0; i < propertyPath.Length; i++)
|
||||
{
|
||||
PropertyInfo pi = type.GetProperty(propertyPath[i])!;
|
||||
expr = Expression.Property(expr, pi);
|
||||
type = pi.PropertyType;
|
||||
}
|
||||
|
||||
Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type);
|
||||
lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression);
|
||||
|
||||
orderByAscending = methodThenBy.MakeGenericMethod(rootType, type);
|
||||
orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootTypeAccessor = typePropSelector[propertyNameLower];
|
||||
orderByAscending = rootTypeAccessor.ThenBy;
|
||||
orderByDescending = rootTypeAccessor.ThenByDescending;
|
||||
lambdaExpression = rootTypeAccessor.KeySelector;
|
||||
lambdaExpression = rootTypeAccessor.Value.KeySelector;
|
||||
}
|
||||
|
||||
var genericMethod = isDesc
|
||||
|
@ -1,8 +1,9 @@
|
||||
using Mapster;
|
||||
using Mapster;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DD.Persistence.Database.Model;
|
||||
using DD.Persistence.Models;
|
||||
using DD.Persistence.Repositories;
|
||||
using System.Text.Json;
|
||||
using DD.Persistence.Models.Common;
|
||||
|
||||
namespace DD.Persistence.Repository.Repositories
|
||||
@ -17,16 +18,33 @@ namespace DD.Persistence.Repository.Repositories
|
||||
|
||||
protected virtual IQueryable<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
|
||||
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(
|
||||
IEnumerable<Guid> setpointKeys,
|
||||
CancellationToken token)
|
||||
{
|
||||
var query = GetQueryReadOnly();
|
||||
|
||||
var entities = await query
|
||||
.Where(e => setpointKeys.Contains(e.Key))
|
||||
.GroupBy(e => e.Key)
|
||||
.Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault())
|
||||
.ToArrayAsync(token);
|
||||
var dtos = entities.Select(e => e.Adapt<SetpointValueDto>());
|
||||
|
||||
var dtos = entities.Select(e => e.Adapt<SetpointValueDto>());
|
||||
return dtos;
|
||||
}
|
||||
public async Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointKeys, CancellationToken token)
|
||||
{
|
||||
var query = GetQueryReadOnly();
|
||||
|
||||
var entities = await query
|
||||
.Where(e => setpointKeys.Contains(e.Key))
|
||||
.GroupBy(e => e.Key)
|
||||
.Select(g => g.OrderByDescending(x => x.Timestamp).FirstOrDefault())
|
||||
.ToDictionaryAsync(x=> x.Key, x => (object)x.Value, token);
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
|
||||
{
|
||||
@ -89,7 +107,7 @@ namespace DD.Persistence.Repository.Repositories
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token)
|
||||
public async Task Add(Guid setpointKey, JsonElement newValue, Guid idUser, CancellationToken token)
|
||||
{
|
||||
var entity = new Setpoint()
|
||||
{
|
||||
@ -102,5 +120,7 @@ namespace DD.Persistence.Repository.Repositories
|
||||
await db.Set<Setpoint>().AddAsync(entity, token);
|
||||
await db.SaveChangesAsync(token);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DD.Persistence.App", "DD.Pe
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Models", "DD.Persistence.Models\DD.Persistence.Models.csproj", "{698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DD.Persistence.Repository.Test", "DD.Persistence.Repository.Test\DD.Persistence.Repository.Test.csproj", "{08B03623-A1C9-482F-B60E-09F293E04999}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
Directory.Build.props = Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -63,6 +70,10 @@ Global
|
||||
{698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{698B4571-BB7A-4A42-8B0B-6C7F2F5360FB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08B03623-A1C9-482F-B60E-09F293E04999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08B03623-A1C9-482F-B60E-09F293E04999}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08B03623-A1C9-482F-B60E-09F293E04999}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08B03623-A1C9-482F-B60E-09F293E04999}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -14,7 +14,7 @@ public interface ISetpointApi : ISyncApi<SetpointLogDto>
|
||||
/// <param name="setpoitKeys">ключи уставок</param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent(IEnumerable<Guid> setpoitKeys, CancellationToken token);
|
||||
Task<ActionResult<Dictionary<Guid, object>>> GetCurrent(IEnumerable<Guid> setpoitKeys, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Получить значения уставок за определенный момент времени
|
||||
|
@ -1,5 +1,6 @@
|
||||
using DD.Persistence.Models;
|
||||
using DD.Persistence.Models.Common;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DD.Persistence.Repositories;
|
||||
|
||||
@ -16,6 +17,14 @@ public interface ISetpointRepository
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Получить значения уставок по набору ключей
|
||||
/// </summary>
|
||||
/// <param name="setpointKeys"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<Dictionary<Guid, object>> GetCurrentDictionary(IEnumerable<Guid> setpointKeys, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Получить значения уставок за определенный момент времени
|
||||
/// </summary>
|
||||
@ -59,5 +68,5 @@ public interface ISetpointRepository
|
||||
/// <returns></returns>
|
||||
/// to do
|
||||
/// id User учесть в соответствующем методе репозитория
|
||||
Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token);
|
||||
Task Add(Guid setpointKey, JsonElement newValue, Guid idUser, CancellationToken token);
|
||||
}
|
||||
|
15
Directory.Build.props
Normal file
15
Directory.Build.props
Normal file
@ -0,0 +1,15 @@
|
||||
<Project>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>$(AssemblyName).Test</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DD.Persistence.IntegrationTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user