diff --git a/DD.Persistence.API/Controllers/SetpointController.cs b/DD.Persistence.API/Controllers/SetpointController.cs index 663a1c4..2f1c6fc 100644 --- a/DD.Persistence.API/Controllers/SetpointController.cs +++ b/DD.Persistence.API/Controllers/SetpointController.cs @@ -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 /// /// [HttpGet("current")] - public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) + public async Task>> GetCurrent([FromQuery] IEnumerable 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 Add(Guid setpointKey, object newValue, CancellationToken token) { var userId = User.GetUserId(); - await setpointRepository.Add(setpointKey, newValue, userId, token); + await setpointRepository.Add(setpointKey, (JsonElement)newValue, userId, token); return CreatedAtAction(nameof(Add), true); } diff --git a/DD.Persistence.API/Readme.md b/DD.Persistence.API/Readme.md new file mode 100644 index 0000000..43bbbf3 --- /dev/null +++ b/DD.Persistence.API/Readme.md @@ -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
Для этого +настройка appsettings.Tests.json должна содержать: +``` +"NeedUseKeyCloak": false, +"AuthUser": { + "username": "myuser", + "password": 12345, + "clientId": "webapi", + "grantType": "password" +} +``` + + diff --git a/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs b/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs index 5745e7e..407eb1c 100644 --- a/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/ISetpointClient.cs @@ -25,12 +25,21 @@ public interface ISetpointClient : IDisposable /// Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); - /// - /// Получить диапазон дат, для которых есть данные в репозитории - /// - /// - /// - Task GetDatesRangeAsync(CancellationToken token); + /// + /// Получить актуальные значения уставок + /// + /// + /// + /// s + Task> GetCurrentDictionary(IEnumerable setpointConfigs, CancellationToken token); + + + /// + /// Получить диапазон дат, для которых есть данные в репозитории + /// + /// + /// + Task GetDatesRangeAsync(CancellationToken token); /// /// Получить значения уставок за определенный момент времени diff --git a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs index 8f75dfa..7931e6d 100644 --- a/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs +++ b/DD.Persistence.Client/Clients/Interfaces/Refit/IRefitSetpointClient.cs @@ -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>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); + [Get($"{BaseRoute}/current")] - Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); + Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, CancellationToken token); [Get($"{BaseRoute}/history")] Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment, CancellationToken token); diff --git a/DD.Persistence.Client/Clients/SetpointClient.cs b/DD.Persistence.Client/Clients/SetpointClient.cs index c43a0b2..dfb5026 100644 --- a/DD.Persistence.Client/Clients/SetpointClient.cs +++ b/DD.Persistence.Client/Clients/SetpointClient.cs @@ -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 refitSetpointClientFactory, ILogger logger) : base(logger) + public SetpointClient( + IRefitClientFactory refitSetpointClientFactory, + ISetpointConfigStorage setpointConfigStorage, + ILogger logger) : base(logger) { this.refitSetpointClient = refitSetpointClientFactory.Create(); - } + this.setpointConfigStorage = setpointConfigStorage; + } public async Task> GetCurrent(IEnumerable 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> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + + + public async Task> GetCurrentDictionary(IEnumerable 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> GetHistory(IEnumerable 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> 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? result) + { + foreach (var log in result) + log.Value = DeserializeValue(log.Key, (JsonElement)log.Value); + + } + + } diff --git a/DD.Persistence.Client/DD.Persistence.Client.csproj b/DD.Persistence.Client/DD.Persistence.Client.csproj index 12bdea4..3491596 100644 --- a/DD.Persistence.Client/DD.Persistence.Client.csproj +++ b/DD.Persistence.Client/DD.Persistence.Client.csproj @@ -11,9 +11,9 @@ DD.Persistence.Client - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1 - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)).1 DD.Persistence.Client @@ -33,15 +33,15 @@ snupkg - C:\Projects\Nuget\Persistence\Client + C:\Projects\Nuget\Persistence Readme.md - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.4.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) diff --git a/DD.Persistence.Client/DependencyInjection.cs b/DD.Persistence.Client/DependencyInjection.cs index 422cf8f..89f65cf 100644 --- a/DD.Persistence.Client/DependencyInjection.cs +++ b/DD.Persistence.Client/DependencyInjection.cs @@ -15,7 +15,7 @@ public static class DependencyInjection /// /// /// - public static IServiceCollection AddPersistenceClients(this IServiceCollection services) + public static IServiceCollection AddPersistenceClients(this IServiceCollection services, Dictionary? setpointTypeConfigs = null) { services.AddTransient(typeof(IRefitClientFactory<>), typeof(RefitClientFactory<>)); services.AddTransient(); @@ -24,6 +24,11 @@ public static class DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + + services.AddSingleton(provider => + { + return new SetpointConfigStorage(setpointTypeConfigs); + }); return services; } } diff --git a/DD.Persistence.Client/ISetpointConfigStorage.cs b/DD.Persistence.Client/ISetpointConfigStorage.cs new file mode 100644 index 0000000..2c73783 --- /dev/null +++ b/DD.Persistence.Client/ISetpointConfigStorage.cs @@ -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); +} diff --git a/DD.Persistence.Client/SetpointConfigStorage.cs b/DD.Persistence.Client/SetpointConfigStorage.cs new file mode 100644 index 0000000..5cfbabf --- /dev/null +++ b/DD.Persistence.Client/SetpointConfigStorage.cs @@ -0,0 +1,20 @@ +namespace DD.Persistence.Client; +internal class SetpointConfigStorage : ISetpointConfigStorage +{ + private readonly Dictionary setpointTypeConfigs; + + public SetpointConfigStorage(Dictionary? setpointTypeConfigs) + { + this.setpointTypeConfigs = setpointTypeConfigs?? new Dictionary(); + } + + public bool TryGetType(Guid id, out Type type) + { + return setpointTypeConfigs.TryGetValue(id, out type); + } + + public void AddOrReplace(Guid id, Type type) + { + setpointTypeConfigs[id] = type; + } +} diff --git a/DD.Persistence.Database/Entity/Setpoint.cs b/DD.Persistence.Database/Entity/Setpoint.cs index e3c2ef5..e526c22 100644 --- a/DD.Persistence.Database/Entity/Setpoint.cs +++ b/DD.Persistence.Database/Entity/Setpoint.cs @@ -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; } diff --git a/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index 5b36d6a..e84402a 100644 --- a/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/DD.Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -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(); + + configStorage = (SetpointConfigStorage)scope.ServiceProvider.GetRequiredService(); } + + [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(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); diff --git a/DD.Persistence.Models/DD.Persistence.Models.csproj b/DD.Persistence.Models/DD.Persistence.Models.csproj index dc4772e..6bcae66 100644 --- a/DD.Persistence.Models/DD.Persistence.Models.csproj +++ b/DD.Persistence.Models/DD.Persistence.Models.csproj @@ -11,9 +11,9 @@ DD.Persistence.Models - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.2.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) - 1.0.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) + 1.2.$([System.DateTime]::UtcNow.ToString(yyMM.ddHH)) DD.Persistence.Models @@ -33,7 +33,7 @@ snupkg - C:\Projects\Nuget\Persistence\Models + C:\Projects\Nuget\Persistence diff --git a/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj b/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj new file mode 100644 index 0000000..4f29d86 --- /dev/null +++ b/DD.Persistence.Repository.Test/DD.Persistence.Repository.Test.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DD.Persistence.Repository.Test/RepositoryTestFixture.cs b/DD.Persistence.Repository.Test/RepositoryTestFixture.cs new file mode 100644 index 0000000..81e3694 --- /dev/null +++ b/DD.Persistence.Repository.Test/RepositoryTestFixture.cs @@ -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() + .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() + .UseNpgsql(dbContainer.GetConnectionString()).Options); + + await forumDbContext.Database.MigrateAsync(); + } + + public async Task DisposeAsync() => await dbContainer.DisposeAsync(); +} + diff --git a/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs b/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs new file mode 100644 index 0000000..6b05ff3 --- /dev/null +++ b/DD.Persistence.Repository.Test/SetpointRepositoryShould.cs @@ -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 +{ + 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() + .ValueKind.ShouldBe(JsonValueKind.Number); + } + + private JsonElement GetJsonFromObject(object value) + { + var jsonString = JsonSerializer.Serialize(value); + var doc = JsonDocument.Parse(jsonString); + return doc.RootElement; + } + +} diff --git a/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs b/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs index bc03e3b..bed529e 100644 --- a/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs +++ b/DD.Persistence.Repository/Extensions/EFExtensionsSortBy.cs @@ -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> 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> 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" /// /// - /// var query = query("Timestamp desc"); + /// var query = query("Date desc"); /// /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( @@ -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" /// /// - /// var query = query("Timestamp desc"); + /// var query = query("Date desc"); /// /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( this IQueryable query, + Func> 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; - } - - /// - /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию. - /// - /// - /// - /// - /// Свойство сортировки. - /// Состоит из названия свойства (в любом регистре) - /// и опционально указания направления сортировки "asc" или "desc" - /// - /// - /// var query = query("Timestamp desc"); - /// - /// Запрос с примененной сортировкой - public static IOrderedQueryable ThenSortBy( - this IOrderedQueryable 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 /// Запрос с примененной сортировкой public static IOrderedQueryable SortBy( this IQueryable query, + Func> 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 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)genericMethod - .Invoke(genericMethod, [query, lambdaExpression])!; - return newQuery; - } + Tuple order = orderMethod + .Invoke(rootType, type, rootTypeAccessor); + orderByAscending = order.Item1; + orderByDescending = order.Item2; - /// - /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию - /// - /// - /// - /// Название свойства (в любом регистре) - /// Сортировать по убыванию - /// Запрос с примененной сортировкой - public static IOrderedQueryable ThenSortBy( - this IOrderedQueryable 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 diff --git a/DD.Persistence.Repository/Repositories/SetpointRepository.cs b/DD.Persistence.Repository/Repositories/SetpointRepository.cs index 8fb703c..8c3ae65 100644 --- a/DD.Persistence.Repository/Repositories/SetpointRepository.cs +++ b/DD.Persistence.Repository/Repositories/SetpointRepository.cs @@ -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 GetQueryReadOnly() => db.Set(); - public async Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token) + public async Task> GetCurrent( + IEnumerable 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()); + var dtos = entities.Select(e => e.Adapt()); return dtos; } + public async Task> GetCurrentDictionary(IEnumerable 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> GetHistory(IEnumerable 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().AddAsync(entity, token); await db.SaveChangesAsync(token); } + + } } diff --git a/DD.Persistence.sln b/DD.Persistence.sln index 5fb5aee..eeb198c 100644 --- a/DD.Persistence.sln +++ b/DD.Persistence.sln @@ -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 diff --git a/DD.Persistence/API/ISetpointApi.cs b/DD.Persistence/API/ISetpointApi.cs index f600f99..138e336 100644 --- a/DD.Persistence/API/ISetpointApi.cs +++ b/DD.Persistence/API/ISetpointApi.cs @@ -14,7 +14,7 @@ public interface ISetpointApi : ISyncApi /// ключи уставок /// /// - Task>> GetCurrent(IEnumerable setpoitKeys, CancellationToken token); + Task>> GetCurrent(IEnumerable setpoitKeys, CancellationToken token); /// /// Получить значения уставок за определенный момент времени diff --git a/DD.Persistence/Repositories/ISetpointRepository.cs b/DD.Persistence/Repositories/ISetpointRepository.cs index ecce8e0..7ece9cb 100644 --- a/DD.Persistence/Repositories/ISetpointRepository.cs +++ b/DD.Persistence/Repositories/ISetpointRepository.cs @@ -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 /// Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrentDictionary(IEnumerable setpointKeys, CancellationToken token); + /// /// Получить значения уставок за определенный момент времени /// @@ -59,5 +68,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task Add(Guid setpointKey, object newValue, Guid idUser, CancellationToken token); + Task Add(Guid setpointKey, JsonElement newValue, Guid idUser, CancellationToken token); } diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..cffb4fe --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + <_Parameter1>$(AssemblyName).Test + + + <_Parameter1>DD.Persistence.IntegrationTests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + \ No newline at end of file