From d746f85fe4c223dfac7689a875a1f8d5f452710f Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 18 Nov 2024 09:39:24 +0500 Subject: [PATCH 01/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Setpoint=20API=20=D0=B8=20=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SetpointController.cs | 48 +++++++++++++ .../Model/IPersistenceDbContext.cs | 3 +- Persistence.Database/Model/ISetpointData.cs | 10 +++ .../Model/PersistenceDbContext.cs | 15 ++-- Persistence.Database/Model/Setpoint.cs | 22 ++++++ .../Model/SetpointDictionary.cs | 18 +++++ Persistence.Repository/Data/SetpointDto.cs | 10 +++ Persistence.Repository/DependencyInjection.cs | 3 +- .../Repositories/SetpointRepository.cs | 70 +++++++++++++++++++ Persistence/Models/SetpointLogDto.cs | 2 +- Persistence/Models/SetpointValueDto.cs | 4 +- .../Repositories/ISetpointRepository.cs | 18 ++--- 12 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 Persistence.API/Controllers/SetpointController.cs create mode 100644 Persistence.Database/Model/ISetpointData.cs create mode 100644 Persistence.Database/Model/Setpoint.cs create mode 100644 Persistence.Database/Model/SetpointDictionary.cs create mode 100644 Persistence.Repository/Data/SetpointDto.cs create mode 100644 Persistence.Repository/Repositories/SetpointRepository.cs diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs new file mode 100644 index 0000000..3e8e94c --- /dev/null +++ b/Persistence.API/Controllers/SetpointController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SetpointController : ControllerBase, ISetpointApi + { + private readonly ISetpointRepository setpointRepository; + + public SetpointController(ISetpointRepository setpointRepository) + { + this.setpointRepository = setpointRepository; + } + + [HttpPost("current")] + public Task>> GetCurrentAsync(IEnumerable setpointKeys, CancellationToken token) + { + throw new NotImplementedException(); + } + + [HttpPost("history")] + public async Task>> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + { + var result = await setpointRepository.GetHistoryAsync(setpointKeys, historyMoment, token); + + return Ok(result); + } + + [HttpPost("log")] + public async Task>>> GetLogAsync([FromBody] IEnumerable setpointKeys, CancellationToken token) + { + var result = await setpointRepository.GetLogAsync(setpointKeys, token); + + return Ok(result); + } + + [HttpPost("save")] + public async Task> SaveAsync(Guid setpointKey, object newValue, CancellationToken token) + { + var result = await setpointRepository.SaveAsync(setpointKey, newValue, token); + + return Ok(result); + } + } +} diff --git a/Persistence.Database/Model/IPersistenceDbContext.cs b/Persistence.Database/Model/IPersistenceDbContext.cs index af837d6..2c1aebb 100644 --- a/Persistence.Database/Model/IPersistenceDbContext.cs +++ b/Persistence.Database/Model/IPersistenceDbContext.cs @@ -11,6 +11,7 @@ namespace Persistence.Database.Model; public interface IPersistenceDbContext : IDisposable { DbSet DataSaub { get; } - DatabaseFacade Database { get; } + DbSet Setpoint { get; } + DatabaseFacade Database { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/Persistence.Database/Model/ISetpointData.cs b/Persistence.Database/Model/ISetpointData.cs new file mode 100644 index 0000000..630af70 --- /dev/null +++ b/Persistence.Database/Model/ISetpointData.cs @@ -0,0 +1,10 @@ +namespace Persistence.Database.Model +{ + public interface ISetpointData + { + public Guid Key { get; set; } + public object Value { get; set; } + public DateTimeOffset Created { get; set; } + public int IdUser { get; set; } + } +} diff --git a/Persistence.Database/Model/PersistenceDbContext.cs b/Persistence.Database/Model/PersistenceDbContext.cs index 3baf334..5f4b965 100644 --- a/Persistence.Database/Model/PersistenceDbContext.cs +++ b/Persistence.Database/Model/PersistenceDbContext.cs @@ -1,17 +1,16 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database; +using Microsoft.EntityFrameworkCore; namespace Persistence.Database.Model; public partial class PersistenceDbContext : DbContext, IPersistenceDbContext { public DbSet DataSaub => Set(); + public DbSet Setpoint => Set(); + + public PersistenceDbContext() + { + } + public PersistenceDbContext(DbContextOptions options) { } diff --git a/Persistence.Database/Model/Setpoint.cs b/Persistence.Database/Model/Setpoint.cs new file mode 100644 index 0000000..50d7f24 --- /dev/null +++ b/Persistence.Database/Model/Setpoint.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Persistence.Database.Model +{ + [PrimaryKey(nameof(Key), nameof(Created))] + public class Setpoint : ISetpointData + { + [Comment("Ключ")] + public Guid Key { get; set; } + + [Column(TypeName = "jsonb"), Comment("Значение уставки")] + public required object Value { get; set; } + + [Comment("Дата изменения уставки")] + public DateTimeOffset Created { get; set; } + + [Comment("Id автора последнего изменения")] + public int IdUser { get; set; } + } +} diff --git a/Persistence.Database/Model/SetpointDictionary.cs b/Persistence.Database/Model/SetpointDictionary.cs new file mode 100644 index 0000000..cd20624 --- /dev/null +++ b/Persistence.Database/Model/SetpointDictionary.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Persistence.Database.Model +{ + public class SetpointDictionary + { + [Key, Comment("Ключ")] + public Guid Key { get; set; } + + [Comment("Наименование")] + public required string Name { get; set; } + + [Comment("Описание")] + public string? Description { get; set; } + } +} diff --git a/Persistence.Repository/Data/SetpointDto.cs b/Persistence.Repository/Data/SetpointDto.cs new file mode 100644 index 0000000..261b1bc --- /dev/null +++ b/Persistence.Repository/Data/SetpointDto.cs @@ -0,0 +1,10 @@ +namespace Persistence.Repository.Data +{ + public class SetpointDto + { + public int Id { get; set; } + public required object Value { get; set; } + public DateTimeOffset Edit { get; set; } + public int IdUser { get; set; } + } +} diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 17904c8..e77edd0 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -21,9 +21,10 @@ public static class DependencyInjection services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString(connectionStringName))); - services.AddScoped(provider => provider.GetRequiredService()); + services.AddScoped(provider => provider.GetRequiredService()); services.AddTransient, TimeSeriesDataRepository>(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs new file mode 100644 index 0000000..04304ce --- /dev/null +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -0,0 +1,70 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Persistence.Database.Model; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.Repository.Repositories +{ + public class SetpointRepository : ISetpointRepository + { + private DbContext db; + public SetpointRepository(DbContext db) + { + this.db = db; + } + + protected virtual IQueryable GetQueryReadOnly() => db.Set(); + + public async Task> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key) && e.Created.Date == historyMoment.Date) + .ToArrayAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } + + public async Task>> GetLogAsync(IEnumerable setpointKeys, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .ToArrayAsync(token); + var dtos = entities + .GroupBy(e => e.Key) + .Select(e => new KeyValuePair>( + e.Key, + e.Select(s => s.Adapt()) + )).ToDictionary(); + + return dtos; + } + + public async Task SaveAsync(Guid setpointKey, object newValue, CancellationToken token) + { + try + { + var entity = new Setpoint() + { + Key = setpointKey, + Value = newValue, + IdUser = 0, // ToDo: откуда тянуть? + Created = DateTimeOffset.Now.ToUniversalTime() + }; + + await db.Set().AddAsync(entity, token); + var result = await db.SaveChangesAsync(token); + + return result; + } + catch(Exception ex) + { + var t = ex.Message; + return 0; + } + } + } +} diff --git a/Persistence/Models/SetpointLogDto.cs b/Persistence/Models/SetpointLogDto.cs index 8c259ca..f825aae 100644 --- a/Persistence/Models/SetpointLogDto.cs +++ b/Persistence/Models/SetpointLogDto.cs @@ -7,6 +7,6 @@ using System.Threading.Tasks; namespace Persistence.Models; public class SetpointLogDto : SetpointValueDto { - public DateTimeOffset Edit { get; set; } + public DateTimeOffset Created { get; set; } public int IdUser { get; set; } } diff --git a/Persistence/Models/SetpointValueDto.cs b/Persistence/Models/SetpointValueDto.cs index 7b49143..ca43330 100644 --- a/Persistence/Models/SetpointValueDto.cs +++ b/Persistence/Models/SetpointValueDto.cs @@ -8,7 +8,7 @@ namespace Persistence.Models; public class SetpointValueDto { - public int Id { get; set; } - public object Value { get; set; } + public Guid Key { get; set; } + public required object Value { get; set; } } diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index 38bbbf4..d315725 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -1,10 +1,4 @@ -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; namespace Persistence.Repositories; @@ -17,19 +11,19 @@ public interface ISetpointRepository /// /// Получить значения уставок за определенный момент времени /// - /// + /// /// дата, на которую получаем данные /// /// - Task> GetHistoryAsync(IEnumerable setpoitKeys, DateTimeOffset historyMoment, CancellationToken token); + Task> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token); /// /// Получить историю изменений значений уставок /// - /// + /// /// /// - Task>> GetLogAsync(IEnumerable setpoitKeys, CancellationToken token); + Task>> GetLogAsync(IEnumerable setpointKeys, CancellationToken token); /// /// Метод сохранения уставки @@ -41,5 +35,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task SaveAsync(Guid setpointKey, int idUser, object newValue, CancellationToken token); + Task SaveAsync(Guid setpointKey, object newValue, CancellationToken token); } From 90c62b9ede7a8799841fb7919ebc8e7e6d3c89d2 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 18 Nov 2024 11:32:57 +0500 Subject: [PATCH 02/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20Setpoint=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseIntegrationTest.cs | 4 +- .../Clients/ISetpointClient.cs | 17 +++++ .../Controllers/SetpointControllerTest.cs | 75 +++++++++++++++++++ .../Persistence.IntegrationTests.csproj | 1 + .../WebAppFactoryFixture.cs | 3 +- .../Repositories/SetpointRepository.cs | 3 +- 6 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 Persistence.IntegrationTests/Clients/ISetpointClient.cs create mode 100644 Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs diff --git a/Persistence.IntegrationTests/BaseIntegrationTest.cs b/Persistence.IntegrationTests/BaseIntegrationTest.cs index a0edda9..8a8f764 100644 --- a/Persistence.IntegrationTests/BaseIntegrationTest.cs +++ b/Persistence.IntegrationTests/BaseIntegrationTest.cs @@ -17,9 +17,9 @@ public abstract class BaseIntegrationTest : IClassFixture, protected BaseIntegrationTest(WebAppFactoryFixture factory) { - //scope = factory.Services.CreateScope(); + scope = factory.Services.CreateScope(); - //dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext = scope.ServiceProvider.GetRequiredService(); } public void Dispose() diff --git a/Persistence.IntegrationTests/Clients/ISetpointClient.cs b/Persistence.IntegrationTests/Clients/ISetpointClient.cs new file mode 100644 index 0000000..831f119 --- /dev/null +++ b/Persistence.IntegrationTests/Clients/ISetpointClient.cs @@ -0,0 +1,17 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.IntegrationTests.Clients +{ + public interface ISetpointClient + { + [Post("/api/Setpoint/history")] + Task>> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment); + + [Post("/api/Setpoint/log")] + Task>>> GetLogAsync(IEnumerable setpoitKeys); + + [Post("/api/Setpoint/save")] + Task> SaveAsync(Guid setpointKey, object newValue); + } +} diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs new file mode 100644 index 0000000..b972c43 --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -0,0 +1,75 @@ +using System.Net; +using Persistence.IntegrationTests.Clients; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers +{ + public class SetpointControllerTest : BaseIntegrationTest + { + private ISetpointClient client; + private class TestObject + { + public string? value1 { get; set; } + public int value2 { get; set; } + public double value3 { get; set; } + } + public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) + { + client = factory.GetHttpClient(string.Empty); + } + + [Fact] + public async Task GetHistoryAsync_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + var historyMoment = DateTimeOffset.Now.ToUniversalTime(); + + //act + var response = await client.GetHistoryAsync(setpointKeys, historyMoment); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task GetLogAsync_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + + //act + var response = await client.GetLogAsync(setpointKeys); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SaveAsync_returns_success() + { + //arrange + var setpointKey = Guid.NewGuid(); + var setpointValue = new TestObject() + { + value1 = "1", + value2 = 2, + value3 = 3.3 + }; + + //act + var response = await client.SaveAsync(setpointKey, setpointValue); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } +} diff --git a/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj b/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj index 2b793de..912eeda 100644 --- a/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj +++ b/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj @@ -24,6 +24,7 @@ + diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index ea6016d..dd95ac0 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -8,6 +8,7 @@ using Persistence.API; using Refit; using System.Net.Http.Headers; using System.Text.Json; +using Persistence.Database.Postgres; namespace Persistence.IntegrationTests; public class WebAppFactoryFixture : WebApplicationFactory @@ -51,7 +52,7 @@ public class WebAppFactoryFixture : WebApplicationFactory var scopedServices = scope.ServiceProvider; var dbContext = scopedServices.GetRequiredService(); - //dbContext.Database.EnsureCreatedAndMigrated(); + dbContext.Database.EnsureCreatedAndMigrated(); //dbContext.Deposits.AddRange(Data.Defaults.Deposits); dbContext.SaveChanges(); }); diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs index 04304ce..ef346e6 100644 --- a/Persistence.Repository/Repositories/SetpointRepository.cs +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -60,9 +60,8 @@ namespace Persistence.Repository.Repositories return result; } - catch(Exception ex) + catch (Exception) { - var t = ex.Message; return 0; } } From 6fd79f6c4a56be9402eb88e46e626b6e6c14b353 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 18 Nov 2024 11:39:27 +0500 Subject: [PATCH 03/15] Fix --- .../Controllers/SetpointControllerTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index b972c43..d82e5af 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -70,6 +70,7 @@ namespace Persistence.IntegrationTests.Controllers //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(1, response.Content); } } } From 9e375b48319b642ee71f76d7b28949ebfe18d988 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 18 Nov 2024 15:05:12 +0500 Subject: [PATCH 04/15] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SetpointController.cs | 18 ++-- Persistence.Database/Entity/ISetpointData.cs | 10 -- Persistence.Database/Entity/Setpoint.cs | 2 +- .../Clients/ISetpointClient.cs | 15 +-- .../Controllers/SetpointControllerTest.cs | 91 +++++++++++++++++-- .../Repositories/SetpointRepository.cs | 19 +++- Persistence/API/ISetpointApi.cs | 8 +- .../Repositories/ISetpointRepository.cs | 29 +++--- 8 files changed, 138 insertions(+), 54 deletions(-) delete mode 100644 Persistence.Database/Entity/ISetpointData.cs diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs index 3e8e94c..519dec9 100644 --- a/Persistence.API/Controllers/SetpointController.cs +++ b/Persistence.API/Controllers/SetpointController.cs @@ -16,31 +16,33 @@ namespace Persistence.API.Controllers } [HttpPost("current")] - public Task>> GetCurrentAsync(IEnumerable setpointKeys, CancellationToken token) + public async Task>> GetCurrent(IEnumerable setpointKeys, CancellationToken token) { - throw new NotImplementedException(); + var result = await setpointRepository.GetCurrent(setpointKeys, token); + + return Ok(result); } [HttpPost("history")] - public async Task>> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + public async Task>> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) { - var result = await setpointRepository.GetHistoryAsync(setpointKeys, historyMoment, token); + var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token); return Ok(result); } [HttpPost("log")] - public async Task>>> GetLogAsync([FromBody] IEnumerable setpointKeys, CancellationToken token) + public async Task>>> GetLog([FromBody] IEnumerable setpointKeys, CancellationToken token) { - var result = await setpointRepository.GetLogAsync(setpointKeys, token); + var result = await setpointRepository.GetLog(setpointKeys, token); return Ok(result); } [HttpPost("save")] - public async Task> SaveAsync(Guid setpointKey, object newValue, CancellationToken token) + public async Task> Save(Guid setpointKey, object newValue, CancellationToken token) { - var result = await setpointRepository.SaveAsync(setpointKey, newValue, token); + var result = await setpointRepository.Save(setpointKey, newValue, 0, token); return Ok(result); } diff --git a/Persistence.Database/Entity/ISetpointData.cs b/Persistence.Database/Entity/ISetpointData.cs deleted file mode 100644 index 630af70..0000000 --- a/Persistence.Database/Entity/ISetpointData.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Persistence.Database.Model -{ - public interface ISetpointData - { - public Guid Key { get; set; } - public object Value { get; set; } - public DateTimeOffset Created { get; set; } - public int IdUser { get; set; } - } -} diff --git a/Persistence.Database/Entity/Setpoint.cs b/Persistence.Database/Entity/Setpoint.cs index 50d7f24..c1b8037 100644 --- a/Persistence.Database/Entity/Setpoint.cs +++ b/Persistence.Database/Entity/Setpoint.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Persistence.Database.Model { [PrimaryKey(nameof(Key), nameof(Created))] - public class Setpoint : ISetpointData + public class Setpoint { [Comment("Ключ")] public Guid Key { get; set; } diff --git a/Persistence.IntegrationTests/Clients/ISetpointClient.cs b/Persistence.IntegrationTests/Clients/ISetpointClient.cs index 831f119..2a526f5 100644 --- a/Persistence.IntegrationTests/Clients/ISetpointClient.cs +++ b/Persistence.IntegrationTests/Clients/ISetpointClient.cs @@ -5,13 +5,16 @@ namespace Persistence.IntegrationTests.Clients { public interface ISetpointClient { - [Post("/api/Setpoint/history")] - Task>> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment); + [Post("/current")] + Task>> GetCurrent(IEnumerable setpointKeys); + + [Post("/history")] + Task>> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment); - [Post("/api/Setpoint/log")] - Task>>> GetLogAsync(IEnumerable setpoitKeys); + [Post("/log")] + Task>>> GetLog(IEnumerable setpoitKeys); - [Post("/api/Setpoint/save")] - Task> SaveAsync(Guid setpointKey, object newValue); + [Post("/save")] + Task> Save(Guid setpointKey, object newValue); } } diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index d82e5af..0401575 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,4 +1,6 @@ using System.Net; +using System.Text.Json; +using Mapster; using Persistence.IntegrationTests.Clients; using Xunit; @@ -10,16 +12,36 @@ namespace Persistence.IntegrationTests.Controllers private class TestObject { public string? value1 { get; set; } - public int value2 { get; set; } - public double value3 { get; set; } + public int? value2 { get; set; } } public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { + factory.ClientOptions.BaseAddress = new Uri($"http://localhost/api/Setpoint"); + client = factory.GetHttpClient(string.Empty); } [Fact] - public async Task GetHistoryAsync_returns_success() + public async Task GetCurrent_returns_success() + { + //arrange + var setpointKeys = new List() + { + Guid.NewGuid(), + Guid.NewGuid() + }; + + //act + var response = await client.GetCurrent(setpointKeys); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); + } + + [Fact] + public async Task GetHistory_returns_success() { //arrange var setpointKeys = new List() @@ -30,14 +52,16 @@ namespace Persistence.IntegrationTests.Controllers var historyMoment = DateTimeOffset.Now.ToUniversalTime(); //act - var response = await client.GetHistoryAsync(setpointKeys, historyMoment); + var response = await client.GetHistory(setpointKeys, historyMoment); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); } [Fact] - public async Task GetLogAsync_returns_success() + public async Task GetLog_returns_success() { //arrange var setpointKeys = new List() @@ -47,30 +71,77 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await client.GetLogAsync(setpointKeys); + var response = await client.GetLog(setpointKeys); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.Empty(response.Content); } [Fact] - public async Task SaveAsync_returns_success() + public async Task Save_returns_success() { //arrange var setpointKey = Guid.NewGuid(); var setpointValue = new TestObject() { value1 = "1", - value2 = 2, - value3 = 3.3 + value2 = 2 }; //act - var response = await client.SaveAsync(setpointKey, setpointValue); + var response = await client.Save(setpointKey, setpointValue); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(1, response.Content); } + + [Fact] + public async Task General_test_success() + { + //save + var setpointKey = Guid.NewGuid(); + var setpointValue = new TestObject() + { + value1 = "1", + value2 = 2 + }; + + var saveResponse = await client.Save(setpointKey, setpointValue); + Assert.Equal(HttpStatusCode.OK, saveResponse.StatusCode); + Assert.Equal(1, saveResponse.Content); + + //current + var currentResponse = await client.GetCurrent([setpointKey]); + Assert.Equal(HttpStatusCode.OK, currentResponse.StatusCode); + + var currentContent = currentResponse.Content; + Assert.NotNull(currentContent); + Assert.NotEmpty(currentContent); + + var currentContentValue = currentContent.FirstOrDefault()?.Value?.ToString(); + Assert.NotNull(currentContentValue); + Assert.NotEmpty(currentContentValue); + + var testObjectValue = JsonSerializer.Deserialize(currentContentValue); + Assert.NotNull(testObjectValue); + Assert.Equal(setpointValue.value1, testObjectValue.value1); + Assert.Equal(setpointValue.value2, testObjectValue.value2); + + //history + var historyMoment = DateTimeOffset.Now.ToUniversalTime(); + var historyResponse = await client.GetHistory([setpointKey], historyMoment); + Assert.Equal(HttpStatusCode.OK, historyResponse.StatusCode); + Assert.NotNull(historyResponse.Content); + Assert.NotEmpty(historyResponse.Content); + + //log + var logResponse = await client.GetLog([setpointKey]); + Assert.Equal(HttpStatusCode.OK, logResponse.StatusCode); + Assert.NotNull(logResponse.Content); + Assert.NotEmpty(logResponse.Content); + } } } diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs index ef346e6..ada1c82 100644 --- a/Persistence.Repository/Repositories/SetpointRepository.cs +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -16,7 +16,18 @@ namespace Persistence.Repository.Repositories protected virtual IQueryable GetQueryReadOnly() => db.Set(); - public async Task> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + public async Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token) + { + var query = GetQueryReadOnly(); + var entities = await query + .Where(e => setpointKeys.Contains(e.Key)) + .ToArrayAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } + + public async Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) { var query = GetQueryReadOnly(); var entities = await query @@ -27,7 +38,7 @@ namespace Persistence.Repository.Repositories return dtos; } - public async Task>> GetLogAsync(IEnumerable setpointKeys, CancellationToken token) + public async Task>> GetLog(IEnumerable setpointKeys, CancellationToken token) { var query = GetQueryReadOnly(); var entities = await query @@ -43,7 +54,7 @@ namespace Persistence.Repository.Repositories return dtos; } - public async Task SaveAsync(Guid setpointKey, object newValue, CancellationToken token) + public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token) { try { @@ -51,7 +62,7 @@ namespace Persistence.Repository.Repositories { Key = setpointKey, Value = newValue, - IdUser = 0, // ToDo: откуда тянуть? + IdUser = idUser, Created = DateTimeOffset.Now.ToUniversalTime() }; diff --git a/Persistence/API/ISetpointApi.cs b/Persistence/API/ISetpointApi.cs index d086557..7af0895 100644 --- a/Persistence/API/ISetpointApi.cs +++ b/Persistence/API/ISetpointApi.cs @@ -14,7 +14,7 @@ public interface ISetpointApi /// ключи уставок /// /// - Task>> GetCurrentAsync(IEnumerable setpoitKeys, CancellationToken token); + Task>> GetCurrent(IEnumerable setpoitKeys, CancellationToken token); /// /// Получить значения уставок за определенный момент времени @@ -23,7 +23,7 @@ public interface ISetpointApi /// дата, на которую получаем данные /// /// - Task>> GetHistoryAsync(IEnumerable setpoitKeys, DateTimeOffset historyMoment, CancellationToken token); + Task>> GetHistory(IEnumerable setpoitKeys, DateTimeOffset historyMoment, CancellationToken token); /// /// Получить историю изменений значений уставок @@ -31,7 +31,7 @@ public interface ISetpointApi /// ключи уставок /// /// - Task>>> GetLogAsync(IEnumerable setpoitKeys, CancellationToken token); + Task>>> GetLog(IEnumerable setpoitKeys, CancellationToken token); /// /// Метод сохранения уставки @@ -40,5 +40,5 @@ public interface ISetpointApi /// значение /// /// - Task> SaveAsync(Guid setpointKey, object newValue, CancellationToken token); + Task> Save(Guid setpointKey, object newValue, CancellationToken token); } diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index d315725..f7439fc 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -7,15 +7,22 @@ namespace Persistence.Repositories; /// public interface ISetpointRepository { - - /// - /// Получить значения уставок за определенный момент времени - /// - /// - /// дата, на которую получаем данные - /// - /// - Task> GetHistoryAsync(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token); + /// + /// Получить значения уставок по набору ключей + /// + /// + /// + /// + Task> GetCurrent(IEnumerable setpointKeys, CancellationToken token); + + /// + /// Получить значения уставок за определенный момент времени + /// + /// + /// дата, на которую получаем данные + /// + /// + Task> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token); /// /// Получить историю изменений значений уставок @@ -23,7 +30,7 @@ public interface ISetpointRepository /// /// /// - Task>> GetLogAsync(IEnumerable setpointKeys, CancellationToken token); + Task>> GetLog(IEnumerable setpointKeys, CancellationToken token); /// /// Метод сохранения уставки @@ -35,5 +42,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task SaveAsync(Guid setpointKey, object newValue, CancellationToken token); + Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); } From 9e55d6791cdbad56db77a6cbd3e4b112a024b51d Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Wed, 20 Nov 2024 15:29:58 +0500 Subject: [PATCH 05/15] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=D0=BC=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SetpointController.cs | 19 +-- Persistence.Database/Entity/Setpoint.cs | 3 +- .../Entity/SetpointDictionary.cs | 18 --- .../Clients/ISetpointClient.cs | 21 ++-- .../Controllers/SetpointControllerTest.cs | 108 +++++++++--------- Persistence.Repository/Data/SetpointDto.cs | 18 +++ .../Repositories/SetpointRepository.cs | 40 +++---- .../Repositories/ISetpointRepository.cs | 2 +- 8 files changed, 116 insertions(+), 113 deletions(-) delete mode 100644 Persistence.Database/Entity/SetpointDictionary.cs diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs index 519dec9..14f966c 100644 --- a/Persistence.API/Controllers/SetpointController.cs +++ b/Persistence.API/Controllers/SetpointController.cs @@ -15,36 +15,37 @@ namespace Persistence.API.Controllers this.setpointRepository = setpointRepository; } - [HttpPost("current")] - public async Task>> GetCurrent(IEnumerable setpointKeys, CancellationToken token) + [HttpGet("current")] + public async Task>> GetCurrent([FromQuery] IEnumerable setpointKeys, CancellationToken token) { var result = await setpointRepository.GetCurrent(setpointKeys, token); return Ok(result); } - [HttpPost("history")] - public async Task>> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment, CancellationToken token) + [HttpGet("history")] + public async Task>> GetHistory([FromQuery] IEnumerable setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token) { var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token); return Ok(result); } - [HttpPost("log")] - public async Task>>> GetLog([FromBody] IEnumerable setpointKeys, CancellationToken token) + [HttpGet("log")] + public async Task>>> GetLog([FromQuery] IEnumerable setpointKeys, CancellationToken token) { var result = await setpointRepository.GetLog(setpointKeys, token); return Ok(result); } - [HttpPost("save")] + [HttpPost] public async Task> Save(Guid setpointKey, object newValue, CancellationToken token) { - var result = await setpointRepository.Save(setpointKey, newValue, 0, token); + // ToDo: вычитка idUser + await setpointRepository.Save(setpointKey, newValue, 0, token); - return Ok(result); + return Ok(); } } } diff --git a/Persistence.Database/Entity/Setpoint.cs b/Persistence.Database/Entity/Setpoint.cs index c1b8037..ef6b5dc 100644 --- a/Persistence.Database/Entity/Setpoint.cs +++ b/Persistence.Database/Entity/Setpoint.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Persistence.Database.Model { @@ -13,7 +12,7 @@ namespace Persistence.Database.Model [Column(TypeName = "jsonb"), Comment("Значение уставки")] public required object Value { get; set; } - [Comment("Дата изменения уставки")] + [Comment("Дата создания уставки")] public DateTimeOffset Created { get; set; } [Comment("Id автора последнего изменения")] diff --git a/Persistence.Database/Entity/SetpointDictionary.cs b/Persistence.Database/Entity/SetpointDictionary.cs deleted file mode 100644 index cd20624..0000000 --- a/Persistence.Database/Entity/SetpointDictionary.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Persistence.Database.Model -{ - public class SetpointDictionary - { - [Key, Comment("Ключ")] - public Guid Key { get; set; } - - [Comment("Наименование")] - public required string Name { get; set; } - - [Comment("Описание")] - public string? Description { get; set; } - } -} diff --git a/Persistence.IntegrationTests/Clients/ISetpointClient.cs b/Persistence.IntegrationTests/Clients/ISetpointClient.cs index 2a526f5..a4f79b5 100644 --- a/Persistence.IntegrationTests/Clients/ISetpointClient.cs +++ b/Persistence.IntegrationTests/Clients/ISetpointClient.cs @@ -3,18 +3,23 @@ using Refit; namespace Persistence.IntegrationTests.Clients { + /// + /// Интерфейс для тестирования API, предназначенного для работы с уставками + /// public interface ISetpointClient { - [Post("/current")] - Task>> GetCurrent(IEnumerable setpointKeys); + private const string BaseRoute = "/api/setpoint"; + + [Get($"{BaseRoute}/current")] + Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); - [Post("/history")] - Task>> GetHistory(IEnumerable setpointKeys, DateTimeOffset historyMoment); + [Get($"{BaseRoute}/history")] + Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment); - [Post("/log")] - Task>>> GetLog(IEnumerable setpoitKeys); + [Get($"{BaseRoute}/log")] + Task>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); - [Post("/save")] - Task> Save(Guid setpointKey, object newValue); + [Post($"{BaseRoute}/")] + Task Save(Guid setpointKey, object newValue); } } diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index 0401575..2fcfc52 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,6 +1,4 @@ using System.Net; -using System.Text.Json; -using Mapster; using Persistence.IntegrationTests.Clients; using Xunit; @@ -16,8 +14,6 @@ namespace Persistence.IntegrationTests.Controllers } public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { - factory.ClientOptions.BaseAddress = new Uri($"http://localhost/api/Setpoint"); - client = factory.GetHttpClient(string.Empty); } @@ -40,6 +36,22 @@ namespace Persistence.IntegrationTests.Controllers Assert.Empty(response.Content); } + [Fact] + public async Task GetCurrent_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + + //act + var response = await client.GetCurrent([setpointKey]); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(response.Content.FirstOrDefault()?.Key, setpointKey); + } + [Fact] public async Task GetHistory_returns_success() { @@ -49,7 +61,7 @@ namespace Persistence.IntegrationTests.Controllers Guid.NewGuid(), Guid.NewGuid() }; - var historyMoment = DateTimeOffset.Now.ToUniversalTime(); + var historyMoment = DateTimeOffset.UtcNow; //act var response = await client.GetHistory(setpointKeys, historyMoment); @@ -60,6 +72,24 @@ namespace Persistence.IntegrationTests.Controllers Assert.Empty(response.Content); } + [Fact] + public async Task GetHistory_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + var historyMoment = DateTimeOffset.UtcNow; + historyMoment = historyMoment.AddDays(1); + + //act + var response = await client.GetHistory([setpointKey], historyMoment); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(response.Content.FirstOrDefault()?.Key, setpointKey); + } + [Fact] public async Task GetLog_returns_success() { @@ -79,8 +109,29 @@ namespace Persistence.IntegrationTests.Controllers Assert.Empty(response.Content); } + [Fact] + public async Task GetLog_AfterSave_returns_success() + { + //arrange + var setpointKey = await Save(); + + //act + var response = await client.GetLog([setpointKey]); + + //assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + Assert.Equal(response.Content.FirstOrDefault().Value.FirstOrDefault()?.Key, setpointKey); + } + [Fact] public async Task Save_returns_success() + { + await Save(); + } + + private async Task Save() { //arrange var setpointKey = Guid.NewGuid(); @@ -95,53 +146,8 @@ namespace Persistence.IntegrationTests.Controllers //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(1, response.Content); - } - [Fact] - public async Task General_test_success() - { - //save - var setpointKey = Guid.NewGuid(); - var setpointValue = new TestObject() - { - value1 = "1", - value2 = 2 - }; - - var saveResponse = await client.Save(setpointKey, setpointValue); - Assert.Equal(HttpStatusCode.OK, saveResponse.StatusCode); - Assert.Equal(1, saveResponse.Content); - - //current - var currentResponse = await client.GetCurrent([setpointKey]); - Assert.Equal(HttpStatusCode.OK, currentResponse.StatusCode); - - var currentContent = currentResponse.Content; - Assert.NotNull(currentContent); - Assert.NotEmpty(currentContent); - - var currentContentValue = currentContent.FirstOrDefault()?.Value?.ToString(); - Assert.NotNull(currentContentValue); - Assert.NotEmpty(currentContentValue); - - var testObjectValue = JsonSerializer.Deserialize(currentContentValue); - Assert.NotNull(testObjectValue); - Assert.Equal(setpointValue.value1, testObjectValue.value1); - Assert.Equal(setpointValue.value2, testObjectValue.value2); - - //history - var historyMoment = DateTimeOffset.Now.ToUniversalTime(); - var historyResponse = await client.GetHistory([setpointKey], historyMoment); - Assert.Equal(HttpStatusCode.OK, historyResponse.StatusCode); - Assert.NotNull(historyResponse.Content); - Assert.NotEmpty(historyResponse.Content); - - //log - var logResponse = await client.GetLog([setpointKey]); - Assert.Equal(HttpStatusCode.OK, logResponse.StatusCode); - Assert.NotNull(logResponse.Content); - Assert.NotEmpty(logResponse.Content); + return setpointKey; } } } diff --git a/Persistence.Repository/Data/SetpointDto.cs b/Persistence.Repository/Data/SetpointDto.cs index 261b1bc..4a20aa4 100644 --- a/Persistence.Repository/Data/SetpointDto.cs +++ b/Persistence.Repository/Data/SetpointDto.cs @@ -1,10 +1,28 @@ namespace Persistence.Repository.Data { + /// + /// Модель для работы с уставкой + /// public class SetpointDto { + /// + /// Идентификатор уставки + /// public int Id { get; set; } + + /// + /// Значение уставки + /// public required object Value { get; set; } + + /// + /// Дата сохранения уставки + /// public DateTimeOffset Edit { get; set; } + + /// + /// Ключ пользователя + /// public int IdUser { get; set; } } } diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs index ada1c82..2c08e71 100644 --- a/Persistence.Repository/Repositories/SetpointRepository.cs +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -31,9 +31,13 @@ namespace Persistence.Repository.Repositories { var query = GetQueryReadOnly(); var entities = await query - .Where(e => setpointKeys.Contains(e.Key) && e.Created.Date == historyMoment.Date) + .Where(e => setpointKeys.Contains(e.Key)) .ToArrayAsync(token); - var dtos = entities.Select(e => e.Adapt()); + var filteredEntities = entities + .GroupBy(e => e.Key) + .Select(e => e.Where(e => e.Created <= historyMoment).Last()); + var dtos = filteredEntities + .Select(e => e.Adapt()); return dtos; } @@ -46,35 +50,23 @@ namespace Persistence.Repository.Repositories .ToArrayAsync(token); var dtos = entities .GroupBy(e => e.Key) - .Select(e => new KeyValuePair>( - e.Key, - e.Select(s => s.Adapt()) - )).ToDictionary(); + .ToDictionary(e => e.Key, v => v.Select(z => z.Adapt())); return dtos; } - public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token) + public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token) { - try + var entity = new Setpoint() { - var entity = new Setpoint() - { - Key = setpointKey, - Value = newValue, - IdUser = idUser, - Created = DateTimeOffset.Now.ToUniversalTime() - }; + Key = setpointKey, + Value = newValue, + IdUser = idUser, + Created = DateTimeOffset.UtcNow + }; - await db.Set().AddAsync(entity, token); - var result = await db.SaveChangesAsync(token); - - return result; - } - catch (Exception) - { - return 0; - } + await db.Set().AddAsync(entity, token); + await db.SaveChangesAsync(token); } } } diff --git a/Persistence/Repositories/ISetpointRepository.cs b/Persistence/Repositories/ISetpointRepository.cs index 6d2690e..1d82b16 100644 --- a/Persistence/Repositories/ISetpointRepository.cs +++ b/Persistence/Repositories/ISetpointRepository.cs @@ -42,5 +42,5 @@ public interface ISetpointRepository /// /// to do /// id User учесть в соответствующем методе репозитория - Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); + Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); } From d9cbc9022dbbc7f479d5e11d715b83875f58d3ee Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Thu, 21 Nov 2024 14:50:36 +0500 Subject: [PATCH 06/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20Persistence.Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Persistence.Client/Clients/ISetpointClient.cs | 24 +++++++++ .../Clients/ITimeSeriesClient.cs | 2 +- Persistence.Client/Helpers/ApiTokenHelper.cs | 43 +++++++++++++++ Persistence.Client/Persistence.Client.csproj | 18 +++++++ .../PersistenceClientFactory.cs | 48 +++++++++++++++++ .../Clients/ISetpointClient.cs | 25 --------- .../Controllers/DataSaubControllerTest.cs | 3 +- .../Controllers/SetpointControllerTest.cs | 27 ++++++---- .../TimeSeriesBaseControllerTest.cs | 30 +++++------ .../Persistence.IntegrationTests.csproj | 1 + .../TestHttpClientFactory.cs | 16 ++++++ .../WebAppFactoryFixture.cs | 54 ++++--------------- Persistence.sln | 8 ++- 13 files changed, 201 insertions(+), 98 deletions(-) create mode 100644 Persistence.Client/Clients/ISetpointClient.cs rename {Persistence.IntegrationTests => Persistence.Client}/Clients/ITimeSeriesClient.cs (88%) create mode 100644 Persistence.Client/Helpers/ApiTokenHelper.cs create mode 100644 Persistence.Client/Persistence.Client.csproj create mode 100644 Persistence.Client/PersistenceClientFactory.cs delete mode 100644 Persistence.IntegrationTests/Clients/ISetpointClient.cs create mode 100644 Persistence.IntegrationTests/TestHttpClientFactory.cs diff --git a/Persistence.Client/Clients/ISetpointClient.cs b/Persistence.Client/Clients/ISetpointClient.cs new file mode 100644 index 0000000..49733f0 --- /dev/null +++ b/Persistence.Client/Clients/ISetpointClient.cs @@ -0,0 +1,24 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.Client.Clients; + +/// +/// Интерфейс для тестирования API, предназначенного для работы с уставками +/// +public interface ISetpointClient +{ + private const string BaseRoute = "/api/setpoint"; + + [Get($"{BaseRoute}/current")] + Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); + + [Get($"{BaseRoute}/history")] + Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment); + + [Get($"{BaseRoute}/log")] + Task>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); + + [Post($"{BaseRoute}/")] + Task Save(Guid setpointKey, object newValue); +} diff --git a/Persistence.IntegrationTests/Clients/ITimeSeriesClient.cs b/Persistence.Client/Clients/ITimeSeriesClient.cs similarity index 88% rename from Persistence.IntegrationTests/Clients/ITimeSeriesClient.cs rename to Persistence.Client/Clients/ITimeSeriesClient.cs index 03b7638..8456fa2 100644 --- a/Persistence.IntegrationTests/Clients/ITimeSeriesClient.cs +++ b/Persistence.Client/Clients/ITimeSeriesClient.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Refit; -namespace Persistence.IntegrationTests.Clients; +namespace Persistence.Client.Clients; public interface ITimeSeriesClient where TDto : class, new() { diff --git a/Persistence.Client/Helpers/ApiTokenHelper.cs b/Persistence.Client/Helpers/ApiTokenHelper.cs new file mode 100644 index 0000000..479ba60 --- /dev/null +++ b/Persistence.Client/Helpers/ApiTokenHelper.cs @@ -0,0 +1,43 @@ +namespace Persistence.Client.Helpers; +public static class ApiTokenHelper +{ + public static string GetAdminUserToken() + { + //var user = new User() + //{ + // Id = 1, + // IdCompany = 1, + // Login = "test_user" + //}; + //var roles = new[] { "root" }; + + return string.Empty; + } + + //private static string CreateToken(User user, IEnumerable roles) + //{ + // var claims = new List + // { + // new("id", user.Id.ToString()), + // new(ClaimsIdentity.DefaultNameClaimType, user.Login), + // new("idCompany", user.IdCompany.ToString()), + // }; + + // claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + + // const string secret = "супер секретный ключ для шифрования"; + + // var key = Encoding.ASCII.GetBytes(secret); + // var tokenDescriptor = new SecurityTokenDescriptor + // { + // Issuer = "a", + // Audience = "a", + // Subject = new ClaimsIdentity(claims), + // Expires = DateTime.UtcNow.AddHours(1), + // SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + // }; + // var tokenHandler = new JwtSecurityTokenHandler(); + // var token = tokenHandler.CreateToken(tokenDescriptor); + // return tokenHandler.WriteToken(token); + //} +} diff --git a/Persistence.Client/Persistence.Client.csproj b/Persistence.Client/Persistence.Client.csproj new file mode 100644 index 0000000..c7ed8f5 --- /dev/null +++ b/Persistence.Client/Persistence.Client.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Persistence.Client/PersistenceClientFactory.cs b/Persistence.Client/PersistenceClientFactory.cs new file mode 100644 index 0000000..9c3be71 --- /dev/null +++ b/Persistence.Client/PersistenceClientFactory.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Refit; +using Persistence.Client.Helpers; + +namespace Persistence.Client +{ + public static class PersistenceClientFactory + { + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions)); + + public static T GetClient(HttpClient client) + { + return RestService.For(client, RefitSettings); + } + + public static T GetClient(string baseUrl) + { + var client = new HttpClient(); + client.BaseAddress = new Uri(baseUrl); + + return RestService.For(client, RefitSettings); + } + + private static HttpClient GetAuthorizedClient() + { + var httpClient = new HttpClient(); + var jwtToken = ApiTokenHelper.GetAdminUserToken(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); + + return httpClient; + } + } +} diff --git a/Persistence.IntegrationTests/Clients/ISetpointClient.cs b/Persistence.IntegrationTests/Clients/ISetpointClient.cs deleted file mode 100644 index a4f79b5..0000000 --- a/Persistence.IntegrationTests/Clients/ISetpointClient.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Persistence.Models; -using Refit; - -namespace Persistence.IntegrationTests.Clients -{ - /// - /// Интерфейс для тестирования API, предназначенного для работы с уставками - /// - public interface ISetpointClient - { - private const string BaseRoute = "/api/setpoint"; - - [Get($"{BaseRoute}/current")] - Task>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); - - [Get($"{BaseRoute}/history")] - Task>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable setpointKeys, [Query] DateTimeOffset historyMoment); - - [Get($"{BaseRoute}/log")] - Task>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable setpointKeys); - - [Post($"{BaseRoute}/")] - Task Save(Guid setpointKey, object newValue); - } -} diff --git a/Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs b/Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs index 025492a..8708960 100644 --- a/Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/DataSaubControllerTest.cs @@ -1,4 +1,5 @@ -using Persistence.Database.Model; +using Persistence.Client; +using Persistence.Database.Model; using Persistence.Repository.Data; using Xunit; diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index 2fcfc52..f865ce2 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -1,12 +1,14 @@ using System.Net; -using Persistence.IntegrationTests.Clients; +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; using Xunit; namespace Persistence.IntegrationTests.Controllers { public class SetpointControllerTest : BaseIntegrationTest { - private ISetpointClient client; + private ISetpointClient setpointClient; private class TestObject { public string? value1 { get; set; } @@ -14,7 +16,12 @@ namespace Persistence.IntegrationTests.Controllers } public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { - client = factory.GetHttpClient(string.Empty); + var scope = factory.Services.CreateScope(); + var httpClient = scope.ServiceProvider + .GetRequiredService() + .CreateClient(); + + setpointClient = PersistenceClientFactory.GetClient(httpClient); } [Fact] @@ -28,7 +35,7 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await client.GetCurrent(setpointKeys); + var response = await setpointClient.GetCurrent(setpointKeys); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -43,7 +50,7 @@ namespace Persistence.IntegrationTests.Controllers var setpointKey = await Save(); //act - var response = await client.GetCurrent([setpointKey]); + var response = await setpointClient.GetCurrent([setpointKey]); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -64,7 +71,7 @@ namespace Persistence.IntegrationTests.Controllers var historyMoment = DateTimeOffset.UtcNow; //act - var response = await client.GetHistory(setpointKeys, historyMoment); + var response = await setpointClient.GetHistory(setpointKeys, historyMoment); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -81,7 +88,7 @@ namespace Persistence.IntegrationTests.Controllers historyMoment = historyMoment.AddDays(1); //act - var response = await client.GetHistory([setpointKey], historyMoment); + var response = await setpointClient.GetHistory([setpointKey], historyMoment); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -101,7 +108,7 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await client.GetLog(setpointKeys); + var response = await setpointClient.GetLog(setpointKeys); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -116,7 +123,7 @@ namespace Persistence.IntegrationTests.Controllers var setpointKey = await Save(); //act - var response = await client.GetLog([setpointKey]); + var response = await setpointClient.GetLog([setpointKey]); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -142,7 +149,7 @@ namespace Persistence.IntegrationTests.Controllers }; //act - var response = await client.Save(setpointKey, setpointValue); + var response = await setpointClient.Save(setpointKey, setpointValue); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs index c63f094..59623a1 100644 --- a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs @@ -1,15 +1,8 @@ -using Mapster; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration.UserSecrets; -using Persistence.IntegrationTests.Clients; -using Persistence.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; +using System.Net; +using Mapster; +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; using Xunit; namespace Persistence.IntegrationTests.Controllers; @@ -17,13 +10,18 @@ public abstract class TimeSeriesBaseControllerTest : BaseIntegrat where TEntity : class where TDto : class, new() { - private ITimeSeriesClient client; + private ITimeSeriesClient timeSeriesClient; public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory) { dbContext.CleanupDbSet(); - client = factory.GetAuthorizedHttpClient>(string.Empty); + var scope = factory.Services.CreateScope(); + var httpClient = scope.ServiceProvider + .GetRequiredService() + .CreateClient(); + + timeSeriesClient = PersistenceClientFactory.GetClient>(httpClient); } public async Task InsertRangeSuccess(TDto dto) @@ -32,7 +30,7 @@ public abstract class TimeSeriesBaseControllerTest : BaseIntegrat var expected = dto.Adapt(); //act - var response = await client.InsertRangeAsync(new TDto[] { expected }); + var response = await timeSeriesClient.InsertRangeAsync(new TDto[] { expected }); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -48,7 +46,7 @@ public abstract class TimeSeriesBaseControllerTest : BaseIntegrat dbContext.SaveChanges(); - var response = await client.GetAsync(beginDate, endDate); + var response = await timeSeriesClient.GetAsync(beginDate, endDate); //assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj b/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj index 912eeda..7d0116e 100644 --- a/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj +++ b/Persistence.IntegrationTests/Persistence.IntegrationTests.csproj @@ -24,6 +24,7 @@ + diff --git a/Persistence.IntegrationTests/TestHttpClientFactory.cs b/Persistence.IntegrationTests/TestHttpClientFactory.cs new file mode 100644 index 0000000..1687f49 --- /dev/null +++ b/Persistence.IntegrationTests/TestHttpClientFactory.cs @@ -0,0 +1,16 @@ +namespace Persistence.IntegrationTests +{ + public class TestHttpClientFactory : IHttpClientFactory + { + private readonly WebAppFactoryFixture factory; + + public TestHttpClientFactory(WebAppFactoryFixture factory) + { + this.factory = factory; + } + public HttpClient CreateClient(string name) + { + return factory.CreateClient(); + } + } +} diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index ac73013..f932b83 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -10,6 +10,8 @@ using Refit; using System.Text.Json; using Persistence.Database.Postgres; using System.Net.Http.Headers; +using Persistence.Client; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Persistence.IntegrationTests; public class WebAppFactoryFixture : WebApplicationFactory @@ -47,57 +49,21 @@ public class WebAppFactoryFixture : WebApplicationFactory services.AddDbContext(options => options.UseNpgsql(connectionString)); - var serviceProvider = services.BuildServiceProvider(); + services.RemoveAll(); + services.AddSingleton(provider => + { + return new TestHttpClientFactory(this); + }); + + var serviceProvider = services.BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); var scopedServices = scope.ServiceProvider; - var dbContext = scopedServices.GetRequiredService(); + var dbContext = scopedServices.GetRequiredService(); dbContext.Database.EnsureCreatedAndMigrated(); //dbContext.Deposits.AddRange(Data.Defaults.Deposits); dbContext.SaveChanges(); }); } - - public override async ValueTask DisposeAsync() - { - var dbContext = new PersistenceDbContext( - new DbContextOptionsBuilder() - .UseNpgsql(connectionString) - .Options); - - await dbContext.Database.EnsureDeletedAsync(); - } - - public T GetHttpClient(string uriSuffix) - { - var httpClient = CreateClient(); - if (string.IsNullOrEmpty(uriSuffix)) - return RestService.For(httpClient, RefitSettings); - - if (httpClient.BaseAddress is not null) - httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix); - - return RestService.For(httpClient, RefitSettings); - } - - public T GetAuthorizedHttpClient(string uriSuffix) - { - var httpClient = GetAuthorizedHttpClient(); - if (string.IsNullOrEmpty(uriSuffix)) - return RestService.For(httpClient, RefitSettings); - - if (httpClient.BaseAddress is not null) - httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix); - - return RestService.For(httpClient, RefitSettings); - } - - private HttpClient GetAuthorizedHttpClient() - { - var httpClient = CreateClient(); - ////var jwtToken = ApiTokenHelper.GetAdminUserToken(); - //httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); - return httpClient; - } } diff --git a/Persistence.sln b/Persistence.sln index ce44190..a8115a8 100644 --- a/Persistence.sln +++ b/Persistence.sln @@ -13,7 +13,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Per EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence.Database.Postgres", "Persistence.Database.Postgres\Persistence.Database.Postgres.csproj", "{CC284D27-162D-490C-B6CF-74D666B7C5F3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database.Postgres", "Persistence.Database.Postgres\Persistence.Database.Postgres.csproj", "{CC284D27-162D-490C-B6CF-74D666B7C5F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence.Client", "Persistence.Client\Persistence.Client.csproj", "{84B68660-48E6-4974-A4E5-517552D9DE23}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,6 +47,10 @@ Global {CC284D27-162D-490C-B6CF-74D666B7C5F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.Build.0 = Release|Any CPU + {84B68660-48E6-4974-A4E5-517552D9DE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84B68660-48E6-4974-A4E5-517552D9DE23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84B68660-48E6-4974-A4E5-517552D9DE23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84B68660-48E6-4974-A4E5-517552D9DE23}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b75714c8354e65fba57231cedd27d3216201fca8 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Fri, 22 Nov 2024 17:52:15 +0500 Subject: [PATCH 07/15] Add TimestampedSetRepository --- .../Controllers/TimestampedSetController.cs | 63 +++++ .../PersistenceDbContext.cs | 12 +- Persistence.Database/EFExtensions.cs | 48 ++++ Persistence.Database/Entity/TimestampedSet.cs | 11 + Persistence.Database/IPersistenceDbContext.cs | 5 + .../Clients/ITimestampedSetClient.cs | 23 ++ .../TimestampedSetControllerTest.cs | 219 ++++++++++++++++++ .../WebAppFactoryFixture.cs | 3 + Persistence.Repository/DependencyInjection.cs | 1 + .../Repositories/TimestampedSetRepository.cs | 110 +++++++++ Persistence/Models/TimestampedSetDto.cs | 8 + .../Repositories/ITimestampedSetRepository.cs | 11 + 12 files changed, 511 insertions(+), 3 deletions(-) create mode 100644 Persistence.API/Controllers/TimestampedSetController.cs create mode 100644 Persistence.Database/EFExtensions.cs create mode 100644 Persistence.Database/Entity/TimestampedSet.cs create mode 100644 Persistence.IntegrationTests/Clients/ITimestampedSetClient.cs create mode 100644 Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs create mode 100644 Persistence.Repository/Repositories/TimestampedSetRepository.cs create mode 100644 Persistence/Models/TimestampedSetDto.cs create mode 100644 Persistence/Repositories/ITimestampedSetRepository.cs diff --git a/Persistence.API/Controllers/TimestampedSetController.cs b/Persistence.API/Controllers/TimestampedSetController.cs new file mode 100644 index 0000000..3d7ce63 --- /dev/null +++ b/Persistence.API/Controllers/TimestampedSetController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Persistence.Models; +using Persistence.Repositories; +using Persistence.Repository.Repositories; +using System.Net; + +namespace Persistence.API.Controllers; + +[ApiController] +[Authorize] +[Route("api/[controller]/{idDiscriminator}")] +public class TimestampedSetController : ControllerBase +{ + private readonly ITimestampedSetRepository repository; + + public TimestampedSetController(ITimestampedSetRepository repository) + { + this.repository = repository; + } + + [HttpPost] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] + public async Task InsertRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable sets, CancellationToken token) + { + var result = await repository.InsertRange(idDiscriminator, sets, token); + return Ok(result); + } + + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + public async Task Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, [FromQuery]IEnumerable? props, int skip, int take, CancellationToken token) + { + var result = await repository.Get(idDiscriminator, geTimestamp, props, skip, take, token); + return Ok(result); + } + + [HttpGet("last")] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + public async Task GetLast(Guid idDiscriminator, [FromQuery]IEnumerable? props, int take, CancellationToken token) + { + var result = await repository.GetLast(idDiscriminator, props, take, token); + return Ok(result); + } + + [HttpGet("datesRange")] + [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) + { + var result = await repository.GetDatesRange(idDiscriminator, token); + return Ok(result); + } + + [HttpGet("count")] + [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public async Task Count(Guid idDiscriminator, CancellationToken token) + { + var result = await repository.Count(idDiscriminator, token); + return Ok(result); + } +} diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index f68be73..22462ed 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; +using Persistence.Database.Entity; using System.Data.Common; namespace Persistence.Database.Model; @@ -6,10 +8,12 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext { public DbSet DataSaub => Set(); + public DbSet TimestampedSets => Set(); + public PersistenceDbContext() : base() { - + } public PersistenceDbContext(DbContextOptions options) @@ -30,7 +34,9 @@ public partial class PersistenceDbContext : DbContext, IPersistenceDbContext { modelBuilder.HasPostgresExtension("adminpack") .HasAnnotation("Relational:Collation", "Russian_Russia.1251"); + + modelBuilder.Entity() + .Property(e => e.Set) + .HasJsonConversion(); } - - } diff --git a/Persistence.Database/EFExtensions.cs b/Persistence.Database/EFExtensions.cs new file mode 100644 index 0000000..c424aa3 --- /dev/null +++ b/Persistence.Database/EFExtensions.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Persistence.Database; + +public static class EFExtensions +{ + private static readonly JsonSerializerOptions jsonSerializerOptions = new() + { + AllowTrailingCommas = true, + WriteIndented = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + }; + + public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasJsonConversion( + this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder) + => HasJsonConversion(builder, jsonSerializerOptions); + + public static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasJsonConversion( + this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder, + JsonSerializerOptions jsonSerializerOptions) + { + builder.HasConversion( + s => JsonSerializer.Serialize(s, jsonSerializerOptions), + s => JsonSerializer.Deserialize(s, jsonSerializerOptions)!); + + ValueComparer valueComparer = new( + (a, b) => + (a != null) && (b != null) + ? a.GetHashCode() == b.GetHashCode() + : (a == null) && (b == null), + i => (i == null) ? -1 : i.GetHashCode(), + i => i); + + builder.Metadata.SetValueComparer(valueComparer); + return builder; + } + +} + diff --git a/Persistence.Database/Entity/TimestampedSet.cs b/Persistence.Database/Entity/TimestampedSet.cs new file mode 100644 index 0000000..c9a0dda --- /dev/null +++ b/Persistence.Database/Entity/TimestampedSet.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Persistence.Database.Entity; + +[Comment("Общая таблица данных временных рядов")] +[PrimaryKey(nameof(IdDiscriminator), nameof(Timestamp))] +public record TimestampedSet( + [property: Comment("Дискриминатор ссылка на тип сохраняемых данных")] Guid IdDiscriminator, + [property: Comment("Отметка времени, строго в UTC")] DateTimeOffset Timestamp, + [property: Column(TypeName = "jsonb"), Comment("Набор сохраняемых данных")] IDictionary Set); diff --git a/Persistence.Database/IPersistenceDbContext.cs b/Persistence.Database/IPersistenceDbContext.cs index 66f34ff..d23656e 100644 --- a/Persistence.Database/IPersistenceDbContext.cs +++ b/Persistence.Database/IPersistenceDbContext.cs @@ -1,8 +1,13 @@ using Microsoft.EntityFrameworkCore; +using Persistence.Database.Entity; using Persistence.Database.Model; +using System.Diagnostics.CodeAnalysis; namespace Persistence.Database; public interface IPersistenceDbContext : IDisposable { DbSet DataSaub { get; } + DbSet TimestampedSets { get; } + DbSet Set<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] TEntity>() where TEntity : class; + Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/Persistence.IntegrationTests/Clients/ITimestampedSetClient.cs b/Persistence.IntegrationTests/Clients/ITimestampedSetClient.cs new file mode 100644 index 0000000..96e2500 --- /dev/null +++ b/Persistence.IntegrationTests/Clients/ITimestampedSetClient.cs @@ -0,0 +1,23 @@ +using Persistence.Models; +using Refit; + +namespace Persistence.IntegrationTests.Clients; +public interface ITimestampedSetClient +{ + private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}"; + + [Post(baseUrl)] + Task> InsertRange(Guid idDiscriminator, IEnumerable sets); + + [Get(baseUrl)] + Task>> Get(Guid idDiscriminator, [Query]DateTimeOffset? geTimestamp, [Query]IEnumerable? props, int skip, int take); + + [Get($"{baseUrl}/last")] + Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? props, int take); + + [Get($"{baseUrl}/count")] + Task> Count(Guid idDiscriminator); + + [Get($"{baseUrl}/datesRange")] + Task> GetDatesRange(Guid idDiscriminator); +} diff --git a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs new file mode 100644 index 0000000..12ebb94 --- /dev/null +++ b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs @@ -0,0 +1,219 @@ +using Microsoft.AspNetCore.Mvc; +using Persistence.IntegrationTests.Clients; +using Persistence.Models; +using Xunit; + +namespace Persistence.IntegrationTests.Controllers; +public class TimestampedSetControllerTest : BaseIntegrationTest +{ + private readonly ITimestampedSetClient client; + + public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory) + { + + client = factory.GetAuthorizedHttpClient(string.Empty).Result; + } + + [Fact] + public async Task InsertRange() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + IEnumerable testSets = Generate(10, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + + // act + var response = await client.InsertRange(idDiscriminator, testSets); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal(testSets.Count(), response.Content); + } + + [Fact] + public async Task Get_without_filter() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Get(idDiscriminator, null, null, 0, int.MaxValue); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(count, items.Count()); + } + + [Fact] + public async Task Get_with_filter_props() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + string[] props = ["A"]; + + // act + var response = await client.Get(idDiscriminator, null, props, 0, int.MaxValue); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(count, items.Count()); + foreach ( var item in items ) + { + Assert.Single(item.Set); + var kv = item.Set.First(); + Assert.Equal("A", kv.Key); + } + } + + [Fact] + public async Task Get_geDate() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + var dateMin = DateTimeOffset.Now; + var dateMax = DateTimeOffset.Now.AddSeconds(count); + IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var tail = testSets.OrderBy(t => t.Timestamp).Skip(count / 2).Take(int.MaxValue); + var geDate = tail.First().Timestamp; + var tolerance = TimeSpan.FromSeconds(1); + var expectedCount = tail.Count(); + + // act + var response = await client.Get(idDiscriminator, geDate, null, 0, int.MaxValue); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + var minDate = items.Min(t => t.Timestamp); + Assert.Equal(geDate, geDate, tolerance); + } + + [Fact] + public async Task Get_with_skip_take() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var expectedCount = count / 2; + + // act + var response = await client.Get(idDiscriminator, null, null, 2, expectedCount); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + } + + + [Fact] + public async Task Get_with_big_skip_take() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + var expectedCount = 1; + int count = 10 + expectedCount; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Get(idDiscriminator, null, null, count - expectedCount, count); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + } + + [Fact] + public async Task GetLast() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var expectedCount = 8; + + // act + var response = await client.GetLast(idDiscriminator, null, expectedCount); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var items = response.Content!; + Assert.Equal(expectedCount, items.Count()); + } + + [Fact] + public async Task GetDatesRange() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 10; + var dateMin = DateTimeOffset.Now; + var dateMax = DateTimeOffset.Now.AddSeconds(count-1); + IEnumerable testSets = Generate(count, dateMin.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + var tolerance = TimeSpan.FromSeconds(1); + + // act + var response = await client.GetDatesRange(idDiscriminator); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var range = response.Content!; + Assert.Equal(dateMin, range.From, tolerance); + Assert.Equal(dateMax, range.To, tolerance); + } + + [Fact] + public async Task Count() + { + // arrange + Guid idDiscriminator = Guid.NewGuid(); + int count = 144; + IEnumerable testSets = Generate(count, DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(7))); + var insertResponse = await client.InsertRange(idDiscriminator, testSets); + + // act + var response = await client.Count(idDiscriminator); + + // assert + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal(count, response.Content); + } + + private static IEnumerable Generate(int n, DateTimeOffset from) + { + for (int i = 0; i < n; i++) + yield return new TimestampedSetDto + ( + from.AddSeconds(i), + new Dictionary{ + {"A", i }, + {"B", i * 1.1 }, + {"C", $"Any{i}" }, + {"D", DateTimeOffset.Now}, + } + ); + } +} diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index 8ac7bf2..740d3c5 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Persistence.API; +using Persistence.Database; using Persistence.Database.Model; using Persistence.Database.Postgres; using Refit; @@ -53,6 +54,8 @@ public class WebAppFactoryFixture : WebApplicationFactory services.AddDbContext(options => options.UseNpgsql(connectionString)); + services.AddScoped(provider => provider.GetRequiredService()); + var serviceProvider = services.BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 8e2f759..3a27ed1 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -16,6 +16,7 @@ public static class DependencyInjection MapsterSetup(); services.AddTransient, TimeSeriesDataCachedRepository>(); + services.AddTransient(); return services; } diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs new file mode 100644 index 0000000..a81f7f1 --- /dev/null +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -0,0 +1,110 @@ +using Microsoft.EntityFrameworkCore; +using Persistence.Database; +using Persistence.Database.Entity; +using Persistence.Models; +using Persistence.Repositories; + +namespace Persistence.Repository.Repositories; +public class TimestampedSetRepository : ITimestampedSetRepository +{ + private readonly IPersistenceDbContext db; + + public TimestampedSetRepository(IPersistenceDbContext db) + { + this.db = db; + } + + public Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token) + { + var entities = sets.Select(set => new TimestampedSet(idDiscriminator, set.Timestamp.ToUniversalTime(), set.Set)); + var dbSet = db.Set(); + dbSet.AddRange(entities); + return db.SaveChangesAsync(token); + } + + public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? props, int skip, int take, CancellationToken token) + { + var dbSet = db.Set(); + var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); + + if (geTimestamp.HasValue) + query = ApplyGeTimestamp(query, geTimestamp.Value); + + var data = await Materialize(query, token); + + if (props is not null && props.Any()) + data = ApplyPropsFilter(data, props); + + return data; + } + + public async Task> GetLast(Guid idDiscriminator, IEnumerable? props, int take, CancellationToken token) + { + var dbSet = db.Set(); + var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); + + query = query.OrderByDescending(entity => entity.Timestamp) + .Take(take) + .OrderBy(entity => entity.Timestamp); + + var data = await Materialize(query, token); + + if (props is not null && props.Any()) + data = ApplyPropsFilter(data, props); + + return data; + } + + public Task Count(Guid idDiscriminator, CancellationToken token) + { + var dbSet = db.Set(); + var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); + return query.CountAsync(token); + } + + public async Task GetDatesRange(Guid idDiscriminator, CancellationToken token) + { + var query = db.Set() + .GroupBy(entity => entity.IdDiscriminator) + .Select(group => new + { + Min = group.Min(entity => entity.Timestamp), + Max = group.Max(entity => entity.Timestamp), + }); + + var item = await query.FirstOrDefaultAsync(token); + if (item is null) + return null; + + return new DatesRangeDto + { + From = item.Min, + To = item.Max, + }; + } + + private static async Task> Materialize(IQueryable query, CancellationToken token) + { + var dtoQuery = query.Select(entity => new TimestampedSetDto(entity.Timestamp, entity.Set)); + var dtos = await dtoQuery.ToArrayAsync(token); + return dtos; + } + + private static IQueryable ApplyGeTimestamp(IQueryable query, DateTimeOffset geTimestamp) + { + var geTimestampUtc = geTimestamp.ToUniversalTime(); + return query.Where(entity => entity.Timestamp >= geTimestampUtc); + } + + private static IEnumerable ApplyPropsFilter(IEnumerable query, IEnumerable props) + { + var newQuery = query + .Select(entity => new TimestampedSetDto( + entity.Timestamp, + entity.Set + .Where(prop => props.Contains(prop.Key)) + .ToDictionary(prop => prop.Key, prop => prop.Value) + )); + return newQuery; + } +} diff --git a/Persistence/Models/TimestampedSetDto.cs b/Persistence/Models/TimestampedSetDto.cs new file mode 100644 index 0000000..3235a4e --- /dev/null +++ b/Persistence/Models/TimestampedSetDto.cs @@ -0,0 +1,8 @@ +namespace Persistence.Models; + +/// +/// набор данных с отметкой времени +/// +/// отметка времени +/// набор данных +public record TimestampedSetDto(DateTimeOffset Timestamp, IDictionary Set); diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs new file mode 100644 index 0000000..3e853f1 --- /dev/null +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -0,0 +1,11 @@ +using Persistence.Models; + +namespace Persistence.Repositories; +public interface ITimestampedSetRepository +{ + Task Count(Guid idDiscriminator, CancellationToken token); + Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? props, int skip, int take, CancellationToken token); + Task GetDatesRange(Guid idDiscriminator, CancellationToken token); + Task> GetLast(Guid idDiscriminator, IEnumerable? props, int take, CancellationToken token); + Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); +} \ No newline at end of file From bd8de9afc2598f5068e09f6a306d41d40d05a86f Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Mon, 25 Nov 2024 09:41:11 +0500 Subject: [PATCH 08/15] Add TimestampedSet documentation --- Persistence.Database/EFExtensions.cs | 7 --- .../Repositories/TimestampedSetRepository.cs | 7 +++ .../Repositories/ITimestampedSetRepository.cs | 48 +++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Persistence.Database/EFExtensions.cs b/Persistence.Database/EFExtensions.cs index c424aa3..d60d768 100644 --- a/Persistence.Database/EFExtensions.cs +++ b/Persistence.Database/EFExtensions.cs @@ -1,13 +1,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; using System.Text.Json; -using System.Threading.Tasks; namespace Persistence.Database; diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs index a81f7f1..a190d71 100644 --- a/Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -5,6 +5,13 @@ using Persistence.Models; using Persistence.Repositories; namespace Persistence.Repository.Repositories; + +/// +/// Репозиторий для хранения разных наборов данных временных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// public class TimestampedSetRepository : ITimestampedSetRepository { private readonly IPersistenceDbContext db; diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs index 3e853f1..2966a82 100644 --- a/Persistence/Repositories/ITimestampedSetRepository.cs +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -1,11 +1,59 @@ using Persistence.Models; namespace Persistence.Repositories; + +/// +/// Репозиторий для хранения разных наборов данных временных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// public interface ITimestampedSetRepository { + /// + /// Количество записей по указанному набору в БД. Для пагинации. + /// + /// + /// + /// Task Count(Guid idDiscriminator, CancellationToken token); + + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Идентификатор набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + /// Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? props, int skip, int take, CancellationToken token); + + /// + /// Диапазон дат за которые есть данные + /// + /// + /// + /// Task GetDatesRange(Guid idDiscriminator, CancellationToken token); + + /// + /// Получить последние данные + /// + /// + /// + /// + /// + /// Task> GetLast(Guid idDiscriminator, IEnumerable? props, int take, CancellationToken token); + + /// + /// Добавление новых данных + /// + /// + /// + /// + /// Task InsertRange(Guid idDiscriminator, IEnumerable sets, CancellationToken token); } \ No newline at end of file From 3806e395eb804cc4e8289d609575fedd16e11b3c Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 25 Nov 2024 10:09:38 +0500 Subject: [PATCH 09/15] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20Per?= =?UTF-8?q?sistence.Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SetpointController.cs | 4 +- Persistence.API/DependencyInjection.cs | 191 +++++++++++++----- .../Properties/launchSettings.json | 2 +- Persistence.API/appsettings.Development.json | 3 +- Persistence.API/appsettings.Tests.json | 5 +- Persistence.Client/Helpers/ApiTokenHelper.cs | 99 +++++---- Persistence.Client/Persistence.Client.csproj | 7 + .../PersistenceClientFactory.cs | 42 ++-- .../ApiTokenHelper.cs | 43 ---- .../Controllers/SetpointControllerTest.cs | 7 +- .../TimeSeriesBaseControllerTest.cs | 7 +- Persistence.IntegrationTests/JwtToken.cs | 8 - Persistence.IntegrationTests/KeyCloakUser.cs | 27 --- .../WebAppFactoryFixture.cs | 109 ++-------- Persistence/Models/Configurations/AuthUser.cs | 12 ++ .../Models/Configurations/JwtParams.cs | 18 ++ Persistence/Models/Configurations/JwtToken.cs | 10 + Persistence/Persistence.csproj | 1 + 18 files changed, 297 insertions(+), 298 deletions(-) delete mode 100644 Persistence.IntegrationTests/ApiTokenHelper.cs delete mode 100644 Persistence.IntegrationTests/JwtToken.cs delete mode 100644 Persistence.IntegrationTests/KeyCloakUser.cs create mode 100644 Persistence/Models/Configurations/AuthUser.cs create mode 100644 Persistence/Models/Configurations/JwtParams.cs create mode 100644 Persistence/Models/Configurations/JwtToken.cs diff --git a/Persistence.API/Controllers/SetpointController.cs b/Persistence.API/Controllers/SetpointController.cs index 14f966c..9a6bd61 100644 --- a/Persistence.API/Controllers/SetpointController.cs +++ b/Persistence.API/Controllers/SetpointController.cs @@ -1,10 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Persistence.Models; using Persistence.Repositories; namespace Persistence.API.Controllers { [ApiController] + [Authorize] [Route("api/[controller]")] public class SetpointController : ControllerBase, ISetpointApi { diff --git a/Persistence.API/DependencyInjection.cs b/Persistence.API/DependencyInjection.cs index e91e3a0..2762881 100644 --- a/Persistence.API/DependencyInjection.cs +++ b/Persistence.API/DependencyInjection.cs @@ -1,7 +1,13 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; +using Persistence.Models; +using Persistence.Models.Configurations; +using System.Data.Common; +using System.Text; using System.Text.Json.Nodes; namespace Persistence.API; @@ -30,59 +36,144 @@ public static class DependencyInjection }); c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" }); - c.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"]), - } - } - }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Keycloack" - }, - Scheme = "Bearer", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); + var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get(); + if (needUseKeyCloak) + { + c.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"]), + } + } + }); - //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - //var includeControllerXmlComment = true; - //c.IncludeXmlComments(xmlPath, includeControllerXmlComment); - //c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); - }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Keycloack" + }, + Scheme = "Bearer", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + } + else + { + c.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", + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + } + + //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + //var includeControllerXmlComment = true; + //c.IncludeXmlComments(xmlPath, includeControllerXmlComment); + //c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); + }); } public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(o => - { - o.RequireHttpsMetadata = false; - o.Audience = configuration["Authentication:Audience"]; - o.MetadataAddress = configuration["Authentication:MetadataAddress"]!; - o.TokenValidationParameters = new TokenValidationParameters - { - ValidIssuer = configuration["Authentication:ValidIssuer"], - }; - }); - } + var needUseKeyCloak = configuration + .GetSection("NeedUseKeyCloak") + .Get(); + if (needUseKeyCloak) 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"], + }; + }); + else services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = JwtParams.Issuer, + ValidateAudience = true, + ValidAudience = JwtParams.Audience, + ValidateLifetime = true, + IssuerSigningKey = JwtParams.SecurityKey, + ValidateIssuerSigningKey = false + }; + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Headers["Authorization"] + .ToString() + .Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty) + .Trim(); + + context.Token = accessToken; + + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + var username = context.Principal?.Claims + .FirstOrDefault(e => e.Type == "username")?.Value; + + var password = context.Principal?.Claims + .FirstOrDefault(e => e.Type == "password")?.Value; + + var keyCloakUser = configuration + .GetSection(nameof(AuthUser)) + .Get()!; + + if (username != keyCloakUser.Username || password != keyCloakUser.Password) + { + context.Fail("username or password did not match"); + } + + return Task.CompletedTask; + } + }; + }); + } } diff --git a/Persistence.API/Properties/launchSettings.json b/Persistence.API/Properties/launchSettings.json index c2ccc25..52a969d 100644 --- a/Persistence.API/Properties/launchSettings.json +++ b/Persistence.API/Properties/launchSettings.json @@ -8,7 +8,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5032" + "applicationUrl": "http://localhost:13616" }, "IIS Express": { "commandName": "IISExpress", diff --git a/Persistence.API/appsettings.Development.json b/Persistence.API/appsettings.Development.json index 0c208ae..896c0b7 100644 --- a/Persistence.API/appsettings.Development.json +++ b/Persistence.API/appsettings.Development.json @@ -4,5 +4,6 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "NeedUseKeyCloak": false } diff --git a/Persistence.API/appsettings.Tests.json b/Persistence.API/appsettings.Tests.json index 033464f..6201a8f 100644 --- a/Persistence.API/appsettings.Tests.json +++ b/Persistence.API/appsettings.Tests.json @@ -1,11 +1,12 @@ -{ +{ "DbConnection": { "Host": "localhost", "Port": 5432, "Username": "postgres", "Password": "q" }, - "KeycloakTestUser": { + "NeedUseKeyCloak": false, + "AuthUser": { "username": "myuser", "password": 12345, "clientId": "webapi", diff --git a/Persistence.Client/Helpers/ApiTokenHelper.cs b/Persistence.Client/Helpers/ApiTokenHelper.cs index 479ba60..e508922 100644 --- a/Persistence.Client/Helpers/ApiTokenHelper.cs +++ b/Persistence.Client/Helpers/ApiTokenHelper.cs @@ -1,43 +1,72 @@ -namespace Persistence.Client.Helpers; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Persistence.Models.Configurations; +using RestSharp; + +namespace Persistence.Client.Helpers; public static class ApiTokenHelper { - public static string GetAdminUserToken() - { - //var user = new User() - //{ - // Id = 1, - // IdCompany = 1, - // Login = "test_user" - //}; - //var roles = new[] { "root" }; + public static void Authorize(this HttpClient httpClient, IConfiguration configuration) + { + var authUser = configuration + .GetSection(nameof(AuthUser)) + .Get()!; + var needUseKeyCloak = configuration + .GetSection("NeedUseKeyCloak") + .Get()!; + var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get() ?? string.Empty; - return string.Empty; - } + var jwtToken = needUseKeyCloak + ? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl) + : authUser.CreateDefaultJwtToken(); - //private static string CreateToken(User user, IEnumerable roles) - //{ - // var claims = new List - // { - // new("id", user.Id.ToString()), - // new(ClaimsIdentity.DefaultNameClaimType, user.Login), - // new("idCompany", user.IdCompany.ToString()), - // }; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); + } - // claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); + private static string CreateDefaultJwtToken(this AuthUser authUser) + { + var claims = new List() + { + new("client_id", authUser.ClientId), + new("username", authUser.Username), + new("password", authUser.Password), + new("grant_type", authUser.GrantType) + }; - // const string secret = "супер секретный ключ для шифрования"; + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = JwtParams.Issuer, + Audience = JwtParams.Audience, + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(JwtParams.SecurityKey, SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } - // var key = Encoding.ASCII.GetBytes(secret); - // var tokenDescriptor = new SecurityTokenDescriptor - // { - // Issuer = "a", - // Audience = "a", - // Subject = new ClaimsIdentity(claims), - // Expires = DateTime.UtcNow.AddHours(1), - // SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) - // }; - // var tokenHandler = new JwtSecurityTokenHandler(); - // var token = tokenHandler.CreateToken(tokenDescriptor); - // return tokenHandler.WriteToken(token); - //} + private static string CreateKeyCloakJwtToken(this AuthUser authUser, string keycloakGetTokenUrl) + { + var restClient = new RestClient(); + + var request = new RestRequest(keycloakGetTokenUrl, Method.Post); + request.AddParameter("username", authUser.Username); + request.AddParameter("password", authUser.Password); + request.AddParameter("client_id", authUser.ClientId); + request.AddParameter("grant_type", authUser.GrantType); + + var keyCloackResponse = restClient.Post(request); + if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content)) + { + var token = JsonSerializer.Deserialize(keyCloackResponse.Content)!; + return token.AccessToken; + } + + return String.Empty; + } } diff --git a/Persistence.Client/Persistence.Client.csproj b/Persistence.Client/Persistence.Client.csproj index c7ed8f5..d699df7 100644 --- a/Persistence.Client/Persistence.Client.csproj +++ b/Persistence.Client/Persistence.Client.csproj @@ -7,12 +7,19 @@ + + + + + + + diff --git a/Persistence.Client/PersistenceClientFactory.cs b/Persistence.Client/PersistenceClientFactory.cs index 9c3be71..7a8daa8 100644 --- a/Persistence.Client/PersistenceClientFactory.cs +++ b/Persistence.Client/PersistenceClientFactory.cs @@ -1,48 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Refit; +using System.Text.Json; +using Microsoft.Extensions.Configuration; using Persistence.Client.Helpers; +using Persistence.Models.Configurations; +using Refit; namespace Persistence.Client { - public static class PersistenceClientFactory + public class PersistenceClientFactory { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; - private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions)); - - public static T GetClient(HttpClient client) + private HttpClient httpClient; + public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IConfiguration configuration) { - return RestService.For(client, RefitSettings); + this.httpClient = httpClientFactory.CreateClient(); + + httpClient.Authorize(configuration); } - public static T GetClient(string baseUrl) + public T GetClient() { - var client = new HttpClient(); - client.BaseAddress = new Uri(baseUrl); - - return RestService.For(client, RefitSettings); - } - - private static HttpClient GetAuthorizedClient() - { - var httpClient = new HttpClient(); - var jwtToken = ApiTokenHelper.GetAdminUserToken(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken); - - return httpClient; + return RestService.For(httpClient, RefitSettings); } } } diff --git a/Persistence.IntegrationTests/ApiTokenHelper.cs b/Persistence.IntegrationTests/ApiTokenHelper.cs deleted file mode 100644 index 3c1fda2..0000000 --- a/Persistence.IntegrationTests/ApiTokenHelper.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Persistence.IntegrationTests; -public static class ApiTokenHelper -{ - //public static string GetAdminUserToken() - //{ - // var user = new User() - // { - // Id = 1, - // IdCompany = 1, - // Login = "test_user" - // }; - // var roles = new[] { "root" }; - - // return CreateToken(user, roles); - //} - - //private static string CreateToken(User user, IEnumerable roles) - //{ - // var claims = new List - // { - // new("id", user.Id.ToString()), - // new(ClaimsIdentity.DefaultNameClaimType, user.Login), - // new("idCompany", user.IdCompany.ToString()), - // }; - - // claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); - - // const string secret = "супер секретный ключ для шифрования"; - - // var key = Encoding.ASCII.GetBytes(secret); - // var tokenDescriptor = new SecurityTokenDescriptor - // { - // Issuer = "a", - // Audience = "a", - // Subject = new ClaimsIdentity(claims), - // Expires = DateTime.UtcNow.AddHours(1), - // SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) - // }; - // var tokenHandler = new JwtSecurityTokenHandler(); - // var token = tokenHandler.CreateToken(tokenDescriptor); - // return tokenHandler.WriteToken(token); - //} -} diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index f865ce2..8df4170 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -17,11 +17,10 @@ namespace Persistence.IntegrationTests.Controllers public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory) { var scope = factory.Services.CreateScope(); - var httpClient = scope.ServiceProvider - .GetRequiredService() - .CreateClient(); + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); - setpointClient = PersistenceClientFactory.GetClient(httpClient); + setpointClient = persistenceClientFactory.GetClient(); } [Fact] diff --git a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs index 6c110cf..9b43b8e 100644 --- a/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TimeSeriesBaseControllerTest.cs @@ -17,11 +17,10 @@ public abstract class TimeSeriesBaseControllerTest : BaseIntegrat dbContext.CleanupDbSet(); var scope = factory.Services.CreateScope(); - var httpClient = scope.ServiceProvider - .GetRequiredService() - .CreateClient(); + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); - timeSeriesClient = PersistenceClientFactory.GetClient>(httpClient); + timeSeriesClient = persistenceClientFactory.GetClient>(); } public async Task InsertRangeSuccess(TDto dto) diff --git a/Persistence.IntegrationTests/JwtToken.cs b/Persistence.IntegrationTests/JwtToken.cs deleted file mode 100644 index 38bf315..0000000 --- a/Persistence.IntegrationTests/JwtToken.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Persistence.IntegrationTests; -public class JwtToken -{ - [JsonPropertyName("access_token")] - public required string AccessToken { get; set; } -} diff --git a/Persistence.IntegrationTests/KeyCloakUser.cs b/Persistence.IntegrationTests/KeyCloakUser.cs deleted file mode 100644 index aa4d335..0000000 --- a/Persistence.IntegrationTests/KeyCloakUser.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Persistence.IntegrationTests; - -/// -/// настройки credentials для пользователя в KeyCloak -/// -public class KeyCloakUser -{ - /// - /// - /// - public required string Username { get; set; } - - /// - /// - /// - public required string Password { get; set; } - - /// - /// - /// - public required string ClientId { get; set; } - - /// - /// - /// - public required string GrantType { get; set; } -} diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index d04509f..b5f6f97 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -3,58 +3,34 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Persistence.API; +using Persistence.Client; using Persistence.Database.Model; using Persistence.Database.Postgres; -using Refit; using RestSharp; -using System.Net.Http.Headers; -using System.Text.Json; -using Persistence.Database.Postgres; -using System.Net.Http.Headers; -using Persistence.Client; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Persistence.IntegrationTests; public class WebAppFactoryFixture : WebApplicationFactory { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - //Converters = { new ValidationResultConverter() } - }; - - private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions)); - - private readonly string connectionString; - private readonly KeyCloakUser keycloakTestUser; - public readonly string KeycloakGetTokenUrl; - - public WebAppFactoryFixture() - { - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.Tests.json") - .Build(); - - var dbConnection = configuration.GetSection("DbConnection").Get()!; - connectionString = dbConnection.GetConnectionString(); - - keycloakTestUser = configuration.GetSection("KeycloakTestUser").Get()!; - - KeycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Value!; - } + private string connectionString = string.Empty; protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureServices(services => + { + builder.ConfigureAppConfiguration((hostingContext, config) => + { + config.AddJsonFile("appsettings.Tests.json"); + + var dbConnection = config.Build().GetSection("DbConnection").Get()!; + connectionString = dbConnection.GetConnectionString(); + }); + + builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); - if (descriptor != null) services.Remove(descriptor); - - services.AddDbContext(options => + services.AddDbContext(options => options.UseNpgsql(connectionString)); services.RemoveAll(); @@ -63,6 +39,8 @@ public class WebAppFactoryFixture : WebApplicationFactory return new TestHttpClientFactory(this); }); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); @@ -71,7 +49,7 @@ public class WebAppFactoryFixture : WebApplicationFactory var dbContext = scopedServices.GetRequiredService(); dbContext.Database.EnsureCreatedAndMigrated(); dbContext.SaveChanges(); - }); + }); } public override async ValueTask DisposeAsync() @@ -83,57 +61,4 @@ public class WebAppFactoryFixture : WebApplicationFactory await dbContext.Database.EnsureDeletedAsync(); } - - public T GetHttpClient(string uriSuffix) - { - var httpClient = CreateClient(); - if (string.IsNullOrEmpty(uriSuffix)) - return RestService.For(httpClient, RefitSettings); - - if (httpClient.BaseAddress is not null) - httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix); - - return RestService.For(httpClient, RefitSettings); - } - - public async Task GetAuthorizedHttpClient(string uriSuffix) - { - var httpClient = await GetAuthorizedHttpClient(); - if (string.IsNullOrEmpty(uriSuffix)) - return RestService.For(httpClient, RefitSettings); - - if (httpClient.BaseAddress is not null) - httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix); - - return RestService.For(httpClient, RefitSettings); - } - - private async Task GetAuthorizedHttpClient() - { - var httpClient = CreateClient(); - var token = await GetTokenAsync(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - - return httpClient; - } - - private async Task GetTokenAsync() - { - var restClient = new RestClient(); - - var request = new RestRequest(KeycloakGetTokenUrl, Method.Post); - request.AddParameter("username", keycloakTestUser.Username); - request.AddParameter("password", keycloakTestUser.Password); - request.AddParameter("client_id", keycloakTestUser.ClientId); - request.AddParameter("grant_type", keycloakTestUser.GrantType); - - var keyCloackResponse = await restClient.PostAsync(request); - if (keyCloackResponse.IsSuccessful && !String.IsNullOrEmpty(keyCloackResponse.Content)) - { - var token = JsonSerializer.Deserialize(keyCloackResponse.Content)!; - return token.AccessToken; - } - - return String.Empty; - } } diff --git a/Persistence/Models/Configurations/AuthUser.cs b/Persistence/Models/Configurations/AuthUser.cs new file mode 100644 index 0000000..86f11c5 --- /dev/null +++ b/Persistence/Models/Configurations/AuthUser.cs @@ -0,0 +1,12 @@ +namespace Persistence.Models.Configurations; + +/// +/// Настройки credentials для авторизации +/// +public class AuthUser +{ + public required string Username { get; set; } + public required string Password { get; set; } + public required string ClientId { get; set; } + public required string GrantType { get; set; } +} diff --git a/Persistence/Models/Configurations/JwtParams.cs b/Persistence/Models/Configurations/JwtParams.cs new file mode 100644 index 0000000..d8b7b72 --- /dev/null +++ b/Persistence/Models/Configurations/JwtParams.cs @@ -0,0 +1,18 @@ +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Persistence.Models.Configurations +{ + public static class JwtParams + { + private static readonly string KeyValue = "супер секретный ключ для шифрования"; + public static SymmetricSecurityKey SecurityKey + { + get { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KeyValue)); } + } + + public static readonly string Issuer = "a"; + + public static readonly string Audience = "a"; + } +} diff --git a/Persistence/Models/Configurations/JwtToken.cs b/Persistence/Models/Configurations/JwtToken.cs new file mode 100644 index 0000000..f787cc3 --- /dev/null +++ b/Persistence/Models/Configurations/JwtToken.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Persistence.Models.Configurations +{ + public class JwtToken + { + [JsonPropertyName("access_token")] + public required string AccessToken { get; set; } + } +} diff --git a/Persistence/Persistence.csproj b/Persistence/Persistence.csproj index aaccee7..c857356 100644 --- a/Persistence/Persistence.csproj +++ b/Persistence/Persistence.csproj @@ -9,6 +9,7 @@ + From 4fa00de88ee1c217a2461dbee6e813aad8e3755d Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 25 Nov 2024 10:30:37 +0500 Subject: [PATCH 10/15] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=D0=BC=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/SetpointControllerTest.cs | 6 +++--- Persistence.Repository/Repositories/SetpointRepository.cs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index 2fcfc52..a33c15d 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -49,7 +49,7 @@ namespace Persistence.IntegrationTests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(response.Content); Assert.NotEmpty(response.Content); - Assert.Equal(response.Content.FirstOrDefault()?.Key, setpointKey); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); } [Fact] @@ -87,7 +87,7 @@ namespace Persistence.IntegrationTests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(response.Content); Assert.NotEmpty(response.Content); - Assert.Equal(response.Content.FirstOrDefault()?.Key, setpointKey); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); } [Fact] @@ -122,7 +122,7 @@ namespace Persistence.IntegrationTests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(response.Content); Assert.NotEmpty(response.Content); - Assert.Equal(response.Content.FirstOrDefault().Value.FirstOrDefault()?.Key, setpointKey); + Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); } [Fact] diff --git a/Persistence.Repository/Repositories/SetpointRepository.cs b/Persistence.Repository/Repositories/SetpointRepository.cs index 2c08e71..7f81c29 100644 --- a/Persistence.Repository/Repositories/SetpointRepository.cs +++ b/Persistence.Repository/Repositories/SetpointRepository.cs @@ -35,6 +35,7 @@ namespace Persistence.Repository.Repositories .ToArrayAsync(token); var filteredEntities = entities .GroupBy(e => e.Key) + .Select(e => e.OrderBy(o => o.Created)) .Select(e => e.Where(e => e.Created <= historyMoment).Last()); var dtos = filteredEntities .Select(e => e.Adapt()); From 23e2f869579f67390ac420dbbc325cc57c3a0be9 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 25 Nov 2024 10:54:42 +0500 Subject: [PATCH 11/15] Fix - SetpointControllerTest --- .../Controllers/SetpointControllerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs index a33c15d..e681a81 100644 --- a/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/SetpointControllerTest.cs @@ -122,7 +122,7 @@ namespace Persistence.IntegrationTests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(response.Content); Assert.NotEmpty(response.Content); - Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key); + Assert.Equal(setpointKey, response.Content.FirstOrDefault().Key); } [Fact] From b79d29f08ed633fc3e12212ec3f22431858741d1 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Mon, 25 Nov 2024 14:29:42 +0500 Subject: [PATCH 12/15] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D0=B0=D0=BC=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Persistence.API/DependencyInjection.cs | 178 ++++++++++-------- .../PersistenceClientFactory.cs | 4 +- .../TestHttpClientFactory.cs | 3 + 3 files changed, 103 insertions(+), 82 deletions(-) diff --git a/Persistence.API/DependencyInjection.cs b/Persistence.API/DependencyInjection.cs index 2762881..cdfca4c 100644 --- a/Persistence.API/DependencyInjection.cs +++ b/Persistence.API/DependencyInjection.cs @@ -1,14 +1,10 @@ +using System.Text.Json.Nodes; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Persistence.Models; using Persistence.Models.Configurations; -using System.Data.Common; -using System.Text; -using System.Text.Json.Nodes; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Persistence.API; @@ -38,96 +34,47 @@ public static class DependencyInjection c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" }); var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get(); - if (needUseKeyCloak) - { - c.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"]), - } - } - }); - - c.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Keycloack" - }, - Scheme = "Bearer", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); - } - else - { - c.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", - }); - - c.AddSecurityRequirement(new OpenApiSecurityRequirement() - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() - } - }); - } + 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); - //c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); + //options.IncludeXmlComments(xmlPath, includeControllerXmlComment); + //options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); }); } - public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) + #region Authentication + public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration) { var needUseKeyCloak = configuration .GetSection("NeedUseKeyCloak") .Get(); - if (needUseKeyCloak) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { + 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"], - }; - }); - else services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + { + ValidIssuer = configuration["Authentication:ValidIssuer"], + }; + }); + } + + private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; @@ -176,4 +123,73 @@ public static class DependencyInjection }; }); } + #endregion + + #region Security (Swagger) + private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration) + { + options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme + { + Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]), + } + } + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Keycloack" + }, + Scheme = "Bearer", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); + } + + 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() + } + }); + } + #endregion } diff --git a/Persistence.Client/PersistenceClientFactory.cs b/Persistence.Client/PersistenceClientFactory.cs index 7a8daa8..e84327d 100644 --- a/Persistence.Client/PersistenceClientFactory.cs +++ b/Persistence.Client/PersistenceClientFactory.cs @@ -1,11 +1,13 @@ using System.Text.Json; using Microsoft.Extensions.Configuration; using Persistence.Client.Helpers; -using Persistence.Models.Configurations; using Refit; namespace Persistence.Client { + /// + /// Фабрика клиентов для доступа к Persistence - сервису + /// public class PersistenceClientFactory { private static readonly JsonSerializerOptions JsonSerializerOptions = new() diff --git a/Persistence.IntegrationTests/TestHttpClientFactory.cs b/Persistence.IntegrationTests/TestHttpClientFactory.cs index 1687f49..287498d 100644 --- a/Persistence.IntegrationTests/TestHttpClientFactory.cs +++ b/Persistence.IntegrationTests/TestHttpClientFactory.cs @@ -1,5 +1,8 @@ namespace Persistence.IntegrationTests { + /// + /// Фабрика HTTP клиентов для интеграционных тестов + /// public class TestHttpClientFactory : IHttpClientFactory { private readonly WebAppFactoryFixture factory; From 2169e592e6dc5b59492fcf4e9549e341aab4223e Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Tue, 26 Nov 2024 10:07:50 +0500 Subject: [PATCH 13/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20ITimestampedSetClient.=20=D0=9F=D0=BE=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B.=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B=D0=B9=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=20IPersistenceDbContext.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Clients/ITimestampedSetClient.cs | 4 ++-- .../PersistenceDbContext.cs | 2 +- Persistence.Database/IPersistenceDbContext.cs | 13 ------------- .../Model/IPersistenceDbContext.cs | 17 ----------------- .../Controllers/TimestampedSetControllerTest.cs | 9 ++++++--- .../WebAppFactoryFixture.cs | 3 --- .../Repositories/TimestampedSetRepository.cs | 10 +++++++--- .../Repositories/ITimestampedSetRepository.cs | 8 ++++---- 8 files changed, 20 insertions(+), 46 deletions(-) delete mode 100644 Persistence.Database/IPersistenceDbContext.cs delete mode 100644 Persistence.Database/Model/IPersistenceDbContext.cs diff --git a/Persistence.Client/Clients/ITimestampedSetClient.cs b/Persistence.Client/Clients/ITimestampedSetClient.cs index 96e2500..37b62be 100644 --- a/Persistence.Client/Clients/ITimestampedSetClient.cs +++ b/Persistence.Client/Clients/ITimestampedSetClient.cs @@ -1,7 +1,7 @@ using Persistence.Models; using Refit; -namespace Persistence.IntegrationTests.Clients; +namespace Persistence.Client.Clients; public interface ITimestampedSetClient { private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}"; @@ -10,7 +10,7 @@ public interface ITimestampedSetClient Task> InsertRange(Guid idDiscriminator, IEnumerable sets); [Get(baseUrl)] - Task>> Get(Guid idDiscriminator, [Query]DateTimeOffset? geTimestamp, [Query]IEnumerable? props, int skip, int take); + Task>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable? props, int skip, int take); [Get($"{baseUrl}/last")] Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? props, int take); diff --git a/Persistence.Database.Postgres/PersistenceDbContext.cs b/Persistence.Database.Postgres/PersistenceDbContext.cs index 053df12..a0cae6a 100644 --- a/Persistence.Database.Postgres/PersistenceDbContext.cs +++ b/Persistence.Database.Postgres/PersistenceDbContext.cs @@ -4,7 +4,7 @@ using Persistence.Database.Entity; using System.Data.Common; namespace Persistence.Database.Model; -public partial class PersistenceDbContext : DbContext, IPersistenceDbContext +public partial class PersistenceDbContext : DbContext { public DbSet DataSaub => Set(); diff --git a/Persistence.Database/IPersistenceDbContext.cs b/Persistence.Database/IPersistenceDbContext.cs deleted file mode 100644 index d23656e..0000000 --- a/Persistence.Database/IPersistenceDbContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Persistence.Database.Entity; -using Persistence.Database.Model; -using System.Diagnostics.CodeAnalysis; - -namespace Persistence.Database; -public interface IPersistenceDbContext : IDisposable -{ - DbSet DataSaub { get; } - DbSet TimestampedSets { get; } - DbSet Set<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] TEntity>() where TEntity : class; - Task SaveChangesAsync(CancellationToken cancellationToken); -} diff --git a/Persistence.Database/Model/IPersistenceDbContext.cs b/Persistence.Database/Model/IPersistenceDbContext.cs deleted file mode 100644 index 2c1aebb..0000000 --- a/Persistence.Database/Model/IPersistenceDbContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Persistence.Database.Model; -public interface IPersistenceDbContext : IDisposable -{ - DbSet DataSaub { get; } - DbSet Setpoint { get; } - DatabaseFacade Database { get; } - Task SaveChangesAsync(CancellationToken cancellationToken); -} diff --git a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs index 12ebb94..aa33e1b 100644 --- a/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs +++ b/Persistence.IntegrationTests/Controllers/TimestampedSetControllerTest.cs @@ -1,5 +1,6 @@ -using Microsoft.AspNetCore.Mvc; -using Persistence.IntegrationTests.Clients; +using Microsoft.Extensions.DependencyInjection; +using Persistence.Client; +using Persistence.Client.Clients; using Persistence.Models; using Xunit; @@ -10,8 +11,10 @@ public class TimestampedSetControllerTest : BaseIntegrationTest public TimestampedSetControllerTest(WebAppFactoryFixture factory) : base(factory) { + var persistenceClientFactory = scope.ServiceProvider + .GetRequiredService(); - client = factory.GetAuthorizedHttpClient(string.Empty).Result; + client = persistenceClientFactory.GetClient(); } [Fact] diff --git a/Persistence.IntegrationTests/WebAppFactoryFixture.cs b/Persistence.IntegrationTests/WebAppFactoryFixture.cs index 1d99a2a..7b12362 100644 --- a/Persistence.IntegrationTests/WebAppFactoryFixture.cs +++ b/Persistence.IntegrationTests/WebAppFactoryFixture.cs @@ -43,9 +43,6 @@ public class WebAppFactoryFixture : WebApplicationFactory services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); - services.AddScoped(provider => provider.GetRequiredService()); - - var serviceProvider = services.BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); var scopedServices = scope.ServiceProvider; diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs index a190d71..ab6e1a8 100644 --- a/Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Persistence.Database; using Persistence.Database.Entity; using Persistence.Models; using Persistence.Repositories; @@ -14,9 +13,9 @@ namespace Persistence.Repository.Repositories; /// public class TimestampedSetRepository : ITimestampedSetRepository { - private readonly IPersistenceDbContext db; + private readonly DbContext db; - public TimestampedSetRepository(IPersistenceDbContext db) + public TimestampedSetRepository(DbContext db) { this.db = db; } @@ -37,6 +36,11 @@ public class TimestampedSetRepository : ITimestampedSetRepository if (geTimestamp.HasValue) query = ApplyGeTimestamp(query, geTimestamp.Value); + query = query + .OrderBy(item => item.Timestamp) + .Skip(skip) + .Take(take); + var data = await Materialize(query, token); if (props is not null && props.Any()) diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs index 2966a82..980e47c 100644 --- a/Persistence/Repositories/ITimestampedSetRepository.cs +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -21,7 +21,7 @@ public interface ITimestampedSetRepository /// /// Получение данных с фильтрацией. Значение фильтра null - отключен /// - /// Идентификатор набора + /// Дискриминатор (идентификатор) набора /// Фильтр позднее даты /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора /// @@ -41,8 +41,8 @@ public interface ITimestampedSetRepository /// /// Получить последние данные /// - /// - /// + /// Дискриминатор (идентификатор) набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора /// /// /// @@ -51,7 +51,7 @@ public interface ITimestampedSetRepository /// /// Добавление новых данных /// - /// + /// Дискриминатор (идентификатор) набора /// /// /// From a07dbae5b87bdae3bc72211855d952b4bbcb70bd Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Tue, 26 Nov 2024 11:24:31 +0500 Subject: [PATCH 14/15] TimestampedSetRepository rename private ApplyPropsFilter to ReduceSetColumnsByNames; Fix TimestampedSetController doc and response type; ITimestampedSetClient Add doc. --- Persistence.Repository/Repositories/TimestampedSetRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs index ab6e1a8..0ba4e5b 100644 --- a/Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -107,7 +107,7 @@ public class TimestampedSetRepository : ITimestampedSetRepository return query.Where(entity => entity.Timestamp >= geTimestampUtc); } - private static IEnumerable ApplyPropsFilter(IEnumerable query, IEnumerable props) + private static IEnumerable ReduceSetColumnsByNames(IEnumerable query, IEnumerable columnNames) { var newQuery = query .Select(entity => new TimestampedSetDto( From 3bb5fc4411b35db19edc583e67b583a6c0864974 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Tue, 26 Nov 2024 11:24:31 +0500 Subject: [PATCH 15/15] TimestampedSetRepository rename private ApplyPropsFilter to ReduceSetColumnsByNames; Fix TimestampedSetController doc and response type; ITimestampedSetClient Add doc. --- .../Controllers/TimestampedSetController.cs | 53 ++++++++++++++++--- .../Clients/ITimestampedSetClient.cs | 43 ++++++++++++++- .../Repositories/TimestampedSetRepository.cs | 16 +++--- .../Repositories/ITimestampedSetRepository.cs | 16 +++--- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/Persistence.API/Controllers/TimestampedSetController.cs b/Persistence.API/Controllers/TimestampedSetController.cs index 3d7ce63..f18e4c8 100644 --- a/Persistence.API/Controllers/TimestampedSetController.cs +++ b/Persistence.API/Controllers/TimestampedSetController.cs @@ -2,11 +2,14 @@ using Microsoft.AspNetCore.Mvc; using Persistence.Models; using Persistence.Repositories; -using Persistence.Repository.Repositories; using System.Net; namespace Persistence.API.Controllers; +/// +/// Хранение наборов данных с отметкой времени. +/// Не оптимизировано под большие данные. +/// [ApiController] [Authorize] [Route("api/[controller]/{idDiscriminator}")] @@ -19,6 +22,14 @@ public class TimestampedSetController : ControllerBase this.repository = repository; } + /// + /// Записать новые данные + /// Предполагается что данные с одним дискриминатором имеют одинаковую структуру + /// + /// Дискриминатор (идентификатор) набора + /// + /// + /// кол-во затронутых записей [HttpPost] [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] public async Task InsertRange([FromRoute]Guid idDiscriminator, [FromBody]IEnumerable sets, CancellationToken token) @@ -27,22 +38,46 @@ public class TimestampedSetController : ControllerBase return Ok(result); } + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// + /// Фильтрованный набор данных с сортировкой по отметке времени [HttpGet] [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - public async Task Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, [FromQuery]IEnumerable? props, int skip, int take, CancellationToken token) + public async Task Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, [FromQuery]IEnumerable? columnNames, int skip, int take, CancellationToken token) { - var result = await repository.Get(idDiscriminator, geTimestamp, props, skip, take, token); + var result = await repository.Get(idDiscriminator, geTimestamp, columnNames, skip, take, token); return Ok(result); } + /// + /// Получить последние данные + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// Фильтрованный набор данных с сортировкой по отметке времени [HttpGet("last")] [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] - public async Task GetLast(Guid idDiscriminator, [FromQuery]IEnumerable? props, int take, CancellationToken token) + public async Task GetLast(Guid idDiscriminator, [FromQuery]IEnumerable? columnNames, int take, CancellationToken token) { - var result = await repository.GetLast(idDiscriminator, props, take, token); + var result = await repository.GetLast(idDiscriminator, columnNames, take, token); return Ok(result); } + /// + /// Диапазон дат за которые есть данные + /// + /// + /// + /// Дата первой и последней записи [HttpGet("datesRange")] [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] @@ -52,8 +87,14 @@ public class TimestampedSetController : ControllerBase return Ok(result); } + /// + /// Количество записей по указанному набору в БД. Для пагинации. + /// + /// Дискриминатор (идентификатор) набора + /// + /// [HttpGet("count")] - [ProducesResponseType(typeof(DatesRangeDto), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NoContent)] public async Task Count(Guid idDiscriminator, CancellationToken token) { diff --git a/Persistence.Client/Clients/ITimestampedSetClient.cs b/Persistence.Client/Clients/ITimestampedSetClient.cs index 37b62be..95e8bd1 100644 --- a/Persistence.Client/Clients/ITimestampedSetClient.cs +++ b/Persistence.Client/Clients/ITimestampedSetClient.cs @@ -2,22 +2,61 @@ using Refit; namespace Persistence.Client.Clients; + +/// +/// Клиент для работы с репозиторием для хранения разных наборов данных рядов. +/// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. +/// idDiscriminator формируют клиенты и только им известно что они обозначают. +/// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. +/// public interface ITimestampedSetClient { private const string baseUrl = "/api/TimestampedSet/{idDiscriminator}"; + /// + /// Добавление новых данных + /// + /// Дискриминатор (идентификатор) набора + /// + /// [Post(baseUrl)] Task> InsertRange(Guid idDiscriminator, IEnumerable sets); + /// + /// Получение данных с фильтрацией. Значение фильтра null - отключен + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр позднее даты + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// + /// [Get(baseUrl)] - Task>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable? props, int skip, int take); + Task>> Get(Guid idDiscriminator, [Query] DateTimeOffset? geTimestamp, [Query] IEnumerable? columnNames, int skip, int take); + /// + /// Получить последние данные + /// + /// Дискриминатор (идентификатор) набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// + /// [Get($"{baseUrl}/last")] - Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? props, int take); + Task>> GetLast(Guid idDiscriminator, [Query] IEnumerable? columnNames, int take); + /// + /// Количество записей по указанному набору в БД. Для пагинации. + /// + /// Дискриминатор (идентификатор) набора + /// [Get($"{baseUrl}/count")] Task> Count(Guid idDiscriminator); + /// + /// Диапазон дат за которые есть данные + /// + /// Дискриминатор (идентификатор) набора + /// [Get($"{baseUrl}/datesRange")] Task> GetDatesRange(Guid idDiscriminator); } diff --git a/Persistence.Repository/Repositories/TimestampedSetRepository.cs b/Persistence.Repository/Repositories/TimestampedSetRepository.cs index ab6e1a8..ad9a6cf 100644 --- a/Persistence.Repository/Repositories/TimestampedSetRepository.cs +++ b/Persistence.Repository/Repositories/TimestampedSetRepository.cs @@ -28,7 +28,7 @@ public class TimestampedSetRepository : ITimestampedSetRepository return db.SaveChangesAsync(token); } - public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? props, int skip, int take, CancellationToken token) + public async Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token) { var dbSet = db.Set(); var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); @@ -43,13 +43,13 @@ public class TimestampedSetRepository : ITimestampedSetRepository var data = await Materialize(query, token); - if (props is not null && props.Any()) - data = ApplyPropsFilter(data, props); + if (columnNames is not null && columnNames.Any()) + data = ReduceSetColumnsByNames(data, columnNames); return data; } - public async Task> GetLast(Guid idDiscriminator, IEnumerable? props, int take, CancellationToken token) + public async Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token) { var dbSet = db.Set(); var query = dbSet.Where(entity => entity.IdDiscriminator == idDiscriminator); @@ -60,8 +60,8 @@ public class TimestampedSetRepository : ITimestampedSetRepository var data = await Materialize(query, token); - if (props is not null && props.Any()) - data = ApplyPropsFilter(data, props); + if (columnNames is not null && columnNames.Any()) + data = ReduceSetColumnsByNames(data, columnNames); return data; } @@ -107,13 +107,13 @@ public class TimestampedSetRepository : ITimestampedSetRepository return query.Where(entity => entity.Timestamp >= geTimestampUtc); } - private static IEnumerable ApplyPropsFilter(IEnumerable query, IEnumerable props) + private static IEnumerable ReduceSetColumnsByNames(IEnumerable query, IEnumerable columnNames) { var newQuery = query .Select(entity => new TimestampedSetDto( entity.Timestamp, entity.Set - .Where(prop => props.Contains(prop.Key)) + .Where(prop => columnNames.Contains(prop.Key)) .ToDictionary(prop => prop.Key, prop => prop.Value) )); return newQuery; diff --git a/Persistence/Repositories/ITimestampedSetRepository.cs b/Persistence/Repositories/ITimestampedSetRepository.cs index 980e47c..27627c3 100644 --- a/Persistence/Repositories/ITimestampedSetRepository.cs +++ b/Persistence/Repositories/ITimestampedSetRepository.cs @@ -3,7 +3,7 @@ namespace Persistence.Repositories; /// -/// Репозиторий для хранения разных наборов данных временных рядов. +/// Репозиторий для хранения разных наборов данных рядов. /// idDiscriminator - идентифицирует конкретный набор данных, прим.: циклы измерения АСИБР, или отчет о DrillTest. /// idDiscriminator формируют клиенты и только им известно что они обозначают. /// Так как данные приходят редко, то их прореживания для построения графиков не предусмотрено. @@ -13,7 +13,7 @@ public interface ITimestampedSetRepository /// /// Количество записей по указанному набору в БД. Для пагинации. /// - /// + /// Дискриминатор (идентификатор) набора /// /// Task Count(Guid idDiscriminator, CancellationToken token); @@ -23,17 +23,17 @@ public interface ITimestampedSetRepository /// /// Дискриминатор (идентификатор) набора /// Фильтр позднее даты - /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора /// /// /// /// - Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? props, int skip, int take, CancellationToken token); - + Task> Get(Guid idDiscriminator, DateTimeOffset? geTimestamp, IEnumerable? columnNames, int skip, int take, CancellationToken token); + /// /// Диапазон дат за которые есть данные /// - /// + /// Дискриминатор (идентификатор) набора /// /// Task GetDatesRange(Guid idDiscriminator, CancellationToken token); @@ -42,11 +42,11 @@ public interface ITimestampedSetRepository /// Получить последние данные /// /// Дискриминатор (идентификатор) набора - /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора + /// Фильтр свойств набора. Можно запросить только некоторые свойства из набора /// /// /// - Task> GetLast(Guid idDiscriminator, IEnumerable? props, int take, CancellationToken token); + Task> GetLast(Guid idDiscriminator, IEnumerable? columnNames, int take, CancellationToken token); /// /// Добавление новых данных